diff --git a/resources/ews/ewsclient/auth/ewsoauth.cpp b/resources/ews/ewsclient/auth/ewsoauth.cpp index c6f4e86cd..453e9ef85 100644 --- a/resources/ews/ewsclient/auth/ewsoauth.cpp +++ b/resources/ews/ewsclient/auth/ewsoauth.cpp @@ -1,521 +1,522 @@ /* 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 #ifdef HAVE_QCA #include "ewspkeyauthjob.h" #endif #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"); #ifdef HAVE_QCA static const auto pkeyAuthSuffix = QStringLiteral(" PKeyAuth/1.0"); static const auto pkeyRedirectUri = QStringLiteral("urn:http-auth:PKeyAuth"); static const QString pkeyPasswordMapKey = QStringLiteral("pkey-password"); #endif 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); #ifdef HAVE_QCA void pkeyAuthResult(KJob *job); #endif 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; #ifdef HAVE_QCA QString mPKeyPassword; #endif EwsOAuth *q_ptr = nullptr; Q_DECLARE_PUBLIC(EwsOAuth) }; void EwsOAuthUrlSchemeHandler::requestStarted(QWebEngineUrlRequestJob *request) { - returnUriReceived(request->requestUrl()); + Q_EMIT returnUriReceived(request->requestUrl()); } void EwsOAuthReplyHandler::networkReplyFinished(QNetworkReply *reply) { #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) const auto networkError = reply->error(); #else const auto networkError = reply->networkError(); #endif if (networkError != 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) #ifdef HAVE_QCA || (url.toString(QUrl::RemoveQuery) == pkeyRedirectUri) #endif ) { qCDebug(EWSCLI_LOG) << QStringLiteral("Found redirect URI - blocking request"); - redirectUriIntercepted(url); + 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); #if QTWEBENGINEWIDGETS_VERSION < QT_VERSION_CHECK(5, 13, 0) mWebProfile.setRequestInterceptor(&mRequestInterceptor); #else mWebProfile.setUrlRequestInterceptor(&mRequestInterceptor); #endif 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); 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; #ifdef HAVE_QCA 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"); } #endif 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; - for (const auto &item : query.queryItems()) { + 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(); #ifdef HAVE_QCA 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; } #endif - mOAuth2.authorizationCallbackReceived(queryToVarmap(url)); + Q_EMIT mOAuth2.authorizationCallbackReceived(queryToVarmap(url)); } #ifdef HAVE_QCA 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(); } - mOAuth2.authorizationCallbackReceived(varmap); + Q_EMIT mOAuth2.authorizationCallbackReceived(varmap); } #endif 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() { - requestWalletMap(); + 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); #ifdef HAVE_QCA if (map.contains(pkeyPasswordMapKey)) { d->mPKeyPassword = map[pkeyPasswordMapKey]; } #endif 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/ewsclient/ewsfinditemrequest.cpp b/resources/ews/ewsclient/ewsfinditemrequest.cpp index 55ab10acf..d5bab519a 100644 --- a/resources/ews/ewsclient/ewsfinditemrequest.cpp +++ b/resources/ews/ewsclient/ewsfinditemrequest.cpp @@ -1,296 +1,296 @@ /* Copyright (C) 2015-2017 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 "ewsfinditemrequest.h" #include #include #include "ewsclient_debug.h" static const QString traversalTypeNames[] = { QStringLiteral("Shallow"), QStringLiteral("Deep"), QStringLiteral("SoftDeleted"), QStringLiteral("Associated") }; class EwsFindItemResponse : public EwsRequest::Response { public: EwsFindItemResponse(QXmlStreamReader &reader); bool parseRootFolder(QXmlStreamReader &reader); EwsItem *readItem(QXmlStreamReader &reader); QList mItems; unsigned mTotalItems; int mNextOffset; int mNextNumerator; int mNextDenominator; bool mIncludesLastItem; }; EwsFindItemRequest::EwsFindItemRequest(EwsClient &client, QObject *parent) : EwsRequest(client, parent) , mTraversal(EwsTraversalShallow) , mPagination(false) , mPageBasePoint(EwsBasePointBeginning) , mPageOffset(0) , mFractional(false) , mMaxItems(-1) , mFracNumerator(0) , mFracDenominator(0) , mTotalItems(0) , mNextOffset(-1) , mNextNumerator(-1) , mNextDenominator(-1) , mIncludesLastItem(false) { } EwsFindItemRequest::~EwsFindItemRequest() { } void EwsFindItemRequest::setFolderId(const EwsId &id) { mFolderId = id; } void EwsFindItemRequest::setItemShape(const EwsItemShape &shape) { mShape = shape; } void EwsFindItemRequest::start() { QString reqString; QXmlStreamWriter writer(&reqString); startSoapDocument(writer); writer.writeStartElement(ewsMsgNsUri, QStringLiteral("FindItem")); writer.writeAttribute(QStringLiteral("Traversal"), traversalTypeNames[mTraversal]); mShape.write(writer); if (mPagination) { writer.writeStartElement(ewsMsgNsUri, QStringLiteral("IndexedPageItemView")); if (mMaxItems > 0) { writer.writeAttribute(QStringLiteral("MaxEntriesReturned"), QString::number(mMaxItems)); } writer.writeAttribute(QStringLiteral("Offset"), QString::number(mPageOffset)); writer.writeAttribute(QStringLiteral("BasePoint"), (mPageBasePoint == EwsBasePointEnd) ? QStringLiteral("End") : QStringLiteral("Beginning")); writer.writeEndElement(); } else if (mFractional) { writer.writeStartElement(ewsMsgNsUri, QStringLiteral("FractionalPageItemView")); if (mMaxItems > 0) { writer.writeAttribute(QStringLiteral("MaxEntriesReturned"), QString::number(mMaxItems)); } writer.writeAttribute(QStringLiteral("Numerator"), QString::number(mFracNumerator)); writer.writeAttribute(QStringLiteral("Denominator"), QString::number(mFracDenominator)); writer.writeEndElement(); } writer.writeStartElement(ewsMsgNsUri, QStringLiteral("ParentFolderIds")); mFolderId.writeFolderIds(writer); writer.writeEndElement(); writer.writeEndElement(); endSoapDocument(writer); qCDebug(EWSCLI_PROTO_LOG) << reqString; qCDebugNC(EWSCLI_REQUEST_LOG) << QStringLiteral("Starting FindItems request (folder: ") << mFolderId << QStringLiteral(")"); prepare(reqString); doSend(); } bool EwsFindItemRequest::parseResult(QXmlStreamReader &reader) { return parseResponseMessage(reader, QStringLiteral("FindItem"), [this](QXmlStreamReader &reader) { return parseItemsResponse(reader); }); } bool EwsFindItemRequest::parseItemsResponse(QXmlStreamReader &reader) { EwsFindItemResponse *resp = new EwsFindItemResponse(reader); if (resp->responseClass() == EwsResponseUnknown) { return false; } mItems = resp->mItems; mTotalItems = resp->mTotalItems; mNextOffset = resp->mNextOffset; mNextNumerator = resp->mNextNumerator; mNextDenominator = resp->mNextDenominator; mIncludesLastItem = resp->mIncludesLastItem; if (EWSCLI_REQUEST_LOG().isDebugEnabled()) { if (resp->isSuccess()) { qCDebugNC(EWSCLI_REQUEST_LOG) << QStringLiteral("Got FindItems response (%1 items, last included: %2)") .arg(mItems.size()).arg(mIncludesLastItem ? QStringLiteral("true") : QStringLiteral("false")); } else { qCDebug(EWSCLI_REQUEST_LOG) << QStringLiteral("Got FindItems response - %1") .arg(resp->responseMessage()); } } return true; } EwsFindItemResponse::EwsFindItemResponse(QXmlStreamReader &reader) : EwsRequest::Response(reader) { while (reader.readNextStartElement()) { if (reader.namespaceUri() != ewsMsgNsUri && reader.namespaceUri() != ewsTypeNsUri) { setErrorMsg(QStringLiteral("Unexpected namespace in %1 element: %2") - .arg(QStringLiteral("ResponseMessage")).arg(reader.namespaceUri().toString())); + .arg(QStringLiteral("ResponseMessage"), reader.namespaceUri().toString())); return; } if (reader.name() == QLatin1String("RootFolder")) { if (!parseRootFolder(reader)) { return; } } else if (!readResponseElement(reader)) { setErrorMsg(QStringLiteral("Failed to read EWS request - invalid response element.")); return; } } } bool EwsFindItemResponse::parseRootFolder(QXmlStreamReader &reader) { if (reader.namespaceUri() != ewsMsgNsUri || reader.name() != QLatin1String("RootFolder")) { return setErrorMsg(QStringLiteral("Failed to read EWS request - expected %1 element (got %2).") - .arg(QStringLiteral("RootFolder")).arg(reader.qualifiedName().toString())); + .arg(QStringLiteral("RootFolder"), reader.qualifiedName().toString())); } if (!reader.attributes().hasAttribute(QStringLiteral("TotalItemsInView")) || !reader.attributes().hasAttribute(QStringLiteral("TotalItemsInView"))) { return setErrorMsg(QStringLiteral("Failed to read EWS request - missing attributes of %1 element.") .arg(QStringLiteral("RootFolder"))); } bool ok; QXmlStreamAttributes attrs = reader.attributes(); mTotalItems = attrs.value(QStringLiteral("TotalItemsInView")).toUInt(&ok); if (!ok) { return setErrorMsg(QStringLiteral("Failed to read EWS request - failed to read %1 attribute.") .arg(QStringLiteral("TotalItemsInView"))); } mIncludesLastItem = attrs.value(QStringLiteral("IncludesLastItemInRange")) == QLatin1String("true"); if (attrs.hasAttribute(QStringLiteral("IndexedPagingOffset"))) { mNextOffset = attrs.value(QStringLiteral("IndexedPagingOffset")).toInt(&ok); if (!ok) { return setErrorMsg(QStringLiteral("Failed to read EWS request - failed to read %1 attribute.") .arg(QStringLiteral("IndexedPagingOffset"))); } } if (attrs.hasAttribute(QStringLiteral("NumeratorOffset"))) { mNextNumerator = attrs.value(QStringLiteral("NumeratorOffset")).toInt(&ok); if (!ok) { return setErrorMsg(QStringLiteral("Failed to read EWS request - failed to read %1 attribute.") .arg(QStringLiteral("NumeratorOffset"))); } } if (attrs.hasAttribute(QStringLiteral("AbsoluteDenominator"))) { mNextDenominator = attrs.value(QStringLiteral("AbsoluteDenominator")).toInt(&ok); if (!ok) { return setErrorMsg(QStringLiteral("Failed to read EWS request - failed to read %1 attribute.") .arg(QStringLiteral("AbsoluteDenominator"))); } } if (!reader.readNextStartElement()) { return setErrorMsg(QStringLiteral("Failed to read EWS request - expected a child element in %1 element.") .arg(QStringLiteral("RootFolder"))); } if (reader.namespaceUri() != ewsTypeNsUri || reader.name() != QLatin1String("Items")) { return setErrorMsg(QStringLiteral("Failed to read EWS request - expected %1 element (got %2).") - .arg(QStringLiteral("Items")).arg(reader.qualifiedName().toString())); + .arg(QStringLiteral("Items"), reader.qualifiedName().toString())); } if (!reader.readNextStartElement()) { // An empty Items element means no items. reader.skipCurrentElement(); return true; } if (reader.namespaceUri() != ewsTypeNsUri) { return setErrorMsg(QStringLiteral("Failed to read EWS request - expected child element from types namespace.")); } do { EwsItem *item = readItem(reader); if (item) { mItems.append(*item); } } while (reader.readNextStartElement()); // Finish the Items element reader.skipCurrentElement(); // Finish the RootFolder element reader.skipCurrentElement(); return true; } EwsItem *EwsFindItemResponse::readItem(QXmlStreamReader &reader) { EwsItem *item = nullptr; const QStringRef readerName = reader.name(); if (readerName == QLatin1String("Item") || readerName == QLatin1String("Message") || readerName == QLatin1String("CalendarItem") || readerName == QLatin1String("Contact") || readerName == QLatin1String("DistributionList") || readerName == QLatin1String("MeetingMessage") || readerName == QLatin1String("MeetingRequest") || readerName == QLatin1String("MeetingResponse") || readerName == QLatin1String("MeetingCancellation") || readerName == QLatin1String("Task")) { qCDebug(EWSCLI_LOG).noquote() << QStringLiteral("Processing %1").arg(readerName.toString()); item = new EwsItem(reader); if (!item->isValid()) { setErrorMsg(QStringLiteral("Failed to read EWS request - invalid %1 element.") .arg(readerName.toString())); delete item; return nullptr; } } else { qCWarning(EWSCLI_LOG).noquote() << QStringLiteral("Unsupported folder type %1").arg(readerName.toString()); reader.skipCurrentElement(); } return item; } diff --git a/resources/ews/ewsclient/ewsgetitemrequest.cpp b/resources/ews/ewsclient/ewsgetitemrequest.cpp index 37d2b5835..4b1cd113a 100644 --- a/resources/ews/ewsclient/ewsgetitemrequest.cpp +++ b/resources/ews/ewsclient/ewsgetitemrequest.cpp @@ -1,151 +1,151 @@ /* 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 "ewsgetitemrequest.h" #include "ewsclient_debug.h" EwsGetItemRequest::EwsGetItemRequest(EwsClient &client, QObject *parent) : EwsRequest(client, parent) { } EwsGetItemRequest::~EwsGetItemRequest() { } void EwsGetItemRequest::setItemIds(const EwsId::List &ids) { mIds = ids; } void EwsGetItemRequest::setItemShape(const EwsItemShape &shape) { mShape = shape; } void EwsGetItemRequest::start() { QString reqString; QXmlStreamWriter writer(&reqString); startSoapDocument(writer); writer.writeStartElement(ewsMsgNsUri, QStringLiteral("GetItem")); mShape.write(writer); writer.writeStartElement(ewsMsgNsUri, QStringLiteral("ItemIds")); for (const EwsId &id : qAsConst(mIds)) { id.writeItemIds(writer); } writer.writeEndElement(); writer.writeEndElement(); endSoapDocument(writer); qCDebug(EWSCLI_PROTO_LOG) << reqString; qCDebugNCS(EWSCLI_REQUEST_LOG) << QStringLiteral("Starting GetItem request (") << mIds << ")"; prepare(reqString); doSend(); } bool EwsGetItemRequest::parseResult(QXmlStreamReader &reader) { return parseResponseMessage(reader, QStringLiteral("GetItem"), [this](QXmlStreamReader &reader) { return parseItemsResponse(reader); }); } bool EwsGetItemRequest::parseItemsResponse(QXmlStreamReader &reader) { Response resp(reader); if (resp.responseClass() == EwsResponseUnknown) { return false; } if (EWSCLI_REQUEST_LOG().isDebugEnabled()) { if (resp.isSuccess()) { const EwsItem &item = resp.item(); const EwsId &id = item[EwsItemFieldItemId].value(); qCDebugNC(EWSCLI_REQUEST_LOG) << QStringLiteral("Got GetItem response (id: %1, subject: %2)") .arg(ewsHash(id.id()), item[EwsItemFieldSubject].toString()); } else { qCDebugNC(EWSCLI_REQUEST_LOG) << QStringLiteral("Got GetItem response - %1") .arg(resp.responseMessage()); } } mResponses.append(resp); return true; } EwsGetItemRequest::Response::Response(QXmlStreamReader &reader) : EwsRequest::Response(reader) { if (mClass == EwsResponseParseError) { return; } while (reader.readNextStartElement()) { if (reader.namespaceUri() != ewsMsgNsUri && reader.namespaceUri() != ewsTypeNsUri) { setErrorMsg(QStringLiteral("Unexpected namespace in %1 element: %2") - .arg(QStringLiteral("ResponseMessage")).arg(reader.namespaceUri().toString())); + .arg(QStringLiteral("ResponseMessage"), reader.namespaceUri().toString())); return; } if (reader.name() == QLatin1String("Items")) { if (!parseItems(reader)) { return; } } else if (!readResponseElement(reader)) { setErrorMsg(QStringLiteral("Failed to read EWS request - invalid response element.")); return; } } } bool EwsGetItemRequest::Response::parseItems(QXmlStreamReader &reader) { if (reader.namespaceUri() != ewsMsgNsUri || reader.name() != QLatin1String("Items")) { return setErrorMsg(QStringLiteral("Failed to read EWS request - expected Items element (got %1).") .arg(reader.qualifiedName().toString())); } if (reader.readNextStartElement()) { if (reader.namespaceUri() != ewsTypeNsUri) { return setErrorMsg(QStringLiteral("Failed to read EWS request - expected child element from types namespace.")); } EwsItem item(reader); if (!item.isValid()) { return setErrorMsg(QStringLiteral("Failed to read EWS request - invalid Item element.")); } mItem = item; // Finish the Items element. reader.skipCurrentElement(); } return true; } diff --git a/resources/ews/ewsclient/ewsgetstreamingeventsrequest.h b/resources/ews/ewsclient/ewsgetstreamingeventsrequest.h index a7dbddafc..9a1e2cb9e 100644 --- a/resources/ews/ewsclient/ewsgetstreamingeventsrequest.h +++ b/resources/ews/ewsclient/ewsgetstreamingeventsrequest.h @@ -1,55 +1,55 @@ /* Copyright (C) 2015-2017 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. */ #ifndef EWSGETSTREAMINGEVENTSREQUEST_H #define EWSGETSTREAMINGEVENTSREQUEST_H #include #include #include "ewseventrequestbase.h" #include "ewsid.h" #include "ewstypes.h" class EwsGetStreamingEventsRequest : public EwsEventRequestBase { Q_OBJECT public: EwsGetStreamingEventsRequest(EwsClient &client, QObject *parent); ~EwsGetStreamingEventsRequest() override; void setTimeout(uint timeout) { mTimeout = timeout; } void start() override; public Q_SLOTS: - void eventsProcessed(const Response &response); + void eventsProcessed(const EwsEventRequestBase::Response &response); Q_SIGNALS: void eventsReceived(KJob *job); protected Q_SLOTS: void requestData(KIO::Job *job, const QByteArray &data) override; void requestDataTimeout(); protected: uint mTimeout; QTimer mRespTimer; }; #endif diff --git a/resources/ews/ewsclient/ewsitem.cpp b/resources/ews/ewsclient/ewsitem.cpp index 162bbe31f..de4597ca8 100644 --- a/resources/ews/ewsclient/ewsitem.cpp +++ b/resources/ews/ewsclient/ewsitem.cpp @@ -1,568 +1,568 @@ /* Copyright (C) 2015-2017 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 "ewsitem.h" #include #include #include "ewsattachment.h" #include "ewsattendee.h" #include "ewsclient_debug.h" #include "ewsitembase_p.h" #include "ewsmailbox.h" #include "ewsoccurrence.h" #include "ewsrecurrence.h" #include "ewsxml.h" #define D_PTR EwsItemPrivate *d = reinterpret_cast(this->d.data()); #define D_CPTR const EwsItemPrivate *d = reinterpret_cast(this->d.data()); class EwsItemPrivate : public EwsItemBasePrivate { public: typedef EwsXml Reader; EwsItemPrivate(); EwsItemPrivate(const EwsItemBasePrivate &other); EwsItemBasePrivate *clone() const override { return new EwsItemPrivate(*this); } static bool bodyReader(QXmlStreamReader &reader, QVariant &val); static bool messageHeadersReader(QXmlStreamReader &reader, QVariant &val); static bool mailboxReader(QXmlStreamReader &reader, QVariant &val); static bool recipientsReader(QXmlStreamReader &reader, QVariant &val); static bool timezoneReader(QXmlStreamReader &reader, QVariant &val); static bool attendeesReader(QXmlStreamReader &reader, QVariant &val); static bool occurrenceReader(QXmlStreamReader &reader, QVariant &val); static bool occurrencesReader(QXmlStreamReader &reader, QVariant &val); static bool recurrenceReader(QXmlStreamReader &reader, QVariant &val); static bool categoriesReader(QXmlStreamReader &reader, QVariant &val); static bool categoriesWriter(QXmlStreamWriter &writer, const QVariant &val); static bool attachmentsReader(QXmlStreamReader &reader, QVariant &val); bool operator==(const EwsItemPrivate &other) const; EwsItemType mType; static const Reader mStaticEwsXml; Reader mEwsXml; }; static const QVector ewsItemItems = { // Item fields {EwsItemFieldMimeContent, QStringLiteral("MimeContent"), &ewsXmlBase64Reader, &ewsXmlBase64Writer}, {EwsItemFieldItemId, QStringLiteral("ItemId"), &ewsXmlIdReader, &ewsXmlIdWriter}, {EwsItemFieldParentFolderId, QStringLiteral("ParentFolderId"), &ewsXmlIdReader, &ewsXmlIdWriter}, {EwsItemFieldItemClass, QStringLiteral("ItemClass"), &ewsXmlTextReader, &ewsXmlTextWriter}, {EwsItemFieldSubject, QStringLiteral("Subject"), &ewsXmlTextReader, &ewsXmlTextWriter}, {EwsItemFieldSensitivity, QStringLiteral("Sensitivity"), &ewsXmlSensitivityReader}, {EwsItemFieldBody, QStringLiteral("Body"), &EwsItemPrivate::bodyReader}, {EwsItemFieldAttachments, QStringLiteral("Attachments"), &EwsItemPrivate::attachmentsReader}, {EwsItemFieldDateTimeReceived, QStringLiteral("DateTimeReceived"), &ewsXmlDateTimeReader}, {EwsItemFieldSize, QStringLiteral("Size"), &ewsXmlUIntReader}, {EwsItemFieldCategories, QStringLiteral("Categories"), &EwsItemPrivate::categoriesReader, &EwsItemPrivate::categoriesWriter}, {EwsItemFieldImportance, QStringLiteral("Importance"), &ewsXmlImportanceReader}, {EwsItemFieldInReplyTo, QStringLiteral("InReplyTo"), &ewsXmlTextReader}, {EwsItemFieldIsDraft, QStringLiteral("IsDraft"), &ewsXmlBoolReader, &ewsXmlBoolWriter}, {EwsItemFieldIsFromMe, QStringLiteral("IsFromMe"), &ewsXmlBoolReader}, {EwsItemFieldInternetMessageHeaders, QStringLiteral("InternetMessageHeaders"), &EwsItemPrivate::messageHeadersReader}, {EwsItemFieldExtendedProperties, QStringLiteral("ExtendedProperty"), &EwsItemBasePrivate::extendedPropertyReader, &EwsItemBasePrivate::extendedPropertyWriter}, {EwsItemFieldHasAttachments, QStringLiteral("HasAttachments"), &ewsXmlBoolReader}, // Message fields {EwsItemFieldToRecipients, QStringLiteral("ToRecipients"), &EwsItemPrivate::recipientsReader}, {EwsItemFieldCcRecipients, QStringLiteral("CcRecipients"), &EwsItemPrivate::recipientsReader}, {EwsItemFieldBccRecipients, QStringLiteral("BccRecipients"), &EwsItemPrivate::recipientsReader}, {EwsItemFieldFrom, QStringLiteral("From"), &EwsItemPrivate::mailboxReader}, {EwsItemFieldInternetMessageId, QStringLiteral("InternetMessageId"), &ewsXmlTextReader}, {EwsItemFieldIsRead, QStringLiteral("IsRead"), &ewsXmlBoolReader, &ewsXmlBoolWriter}, {EwsItemFieldReferences, QStringLiteral("References"), &ewsXmlTextReader}, {EwsItemFieldReplyTo, QStringLiteral("ReplyTo"), &EwsItemPrivate::mailboxReader}, // CalendarItem fields {EwsItemFieldCalendarItemType, QStringLiteral("CalendarItemType"), &ewsXmlCalendarItemTypeReader}, {EwsItemFieldUID, QStringLiteral("UID"), &ewsXmlTextReader}, {EwsItemFieldCulture, QStringLiteral("Culture"), &ewsXmlTextReader}, {EwsItemFieldStartTimeZone, QStringLiteral("StartTimeZone"), &EwsItemPrivate::timezoneReader}, {EwsItemFieldOrganizer, QStringLiteral("Organizer"), &EwsItemPrivate::mailboxReader}, {EwsItemFieldRequiredAttendees, QStringLiteral("RequiredAttendees"), &EwsItemPrivate::attendeesReader}, {EwsItemFieldOptionalAttendees, QStringLiteral("OptionalAttendees"), &EwsItemPrivate::attendeesReader}, {EwsItemFieldResources, QStringLiteral("Resources"), &EwsItemPrivate::attendeesReader}, {EwsItemFieldStart, QStringLiteral("Start"), &ewsXmlDateTimeReader}, {EwsItemFieldEnd, QStringLiteral("End"), &ewsXmlDateTimeReader}, {EwsItemFieldRecurrenceId, QStringLiteral("RecurrenceId"), &ewsXmlDateTimeReader}, {EwsItemFieldIsAllDayEvent, QStringLiteral("IsAllDayEvent"), &ewsXmlBoolReader}, {EwsItemFieldLegacyFreeBusyStatus, QStringLiteral("LegacyFreeBusyStatus"), &ewsXmlLegacyFreeBusyStatusReader}, {EwsItemFieldMyResponseType, QStringLiteral("MyResponseType"), &ewsXmlResponseTypeReader}, {EwsItemFieldAppointmentSequenceNumber, QStringLiteral("AppointmentSequenceNumber"), &ewsXmlUIntReader}, {EwsItemFieldRecurrence, QStringLiteral("Recurrence"), &EwsItemPrivate::recurrenceReader}, {EwsItemFieldFirstOccurrence, QStringLiteral("FirstOccurrence"), &EwsItemPrivate::occurrenceReader}, {EwsItemFieldLastOccurrence, QStringLiteral("LastOccurrence"), &EwsItemPrivate::occurrenceReader}, {EwsItemFieldModifiedOccurrences, QStringLiteral("ModifiedOccurrences"), &EwsItemPrivate::occurrencesReader}, {EwsItemFieldDeletedOccurrences, QStringLiteral("DeletedOccurrences"), &EwsItemPrivate::occurrencesReader}, {EwsItemFieldTimeZone, QStringLiteral("TimeZone"), &ewsXmlTextReader}, {EwsItemFieldExchangePersonIdGuid, QStringLiteral("ExchangePersonIdGuid"), &ewsXmlTextReader}, {EwsItemFieldDoNotForwardMeeting, QStringLiteral("DoNotForwardMeeting"), &ewsXmlBoolReader}, }; const EwsItemPrivate::Reader EwsItemPrivate::mStaticEwsXml(ewsItemItems); EwsItemPrivate::EwsItemPrivate() : EwsItemBasePrivate() , mType(EwsItemTypeUnknown) , mEwsXml(mStaticEwsXml) { } bool EwsItemPrivate::bodyReader(QXmlStreamReader &reader, QVariant &val) { QVariantList vl; QStringRef bodyType = reader.attributes().value(QStringLiteral("BodyType")); if (bodyType.isNull()) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - missing %2 attribute") .arg(QStringLiteral("Body"), QStringLiteral("BodyType")); return false; } bool isHtml; if (bodyType == QLatin1String("HTML")) { isHtml = true; } else if (bodyType == QLatin1String("Text")) { isHtml = false; } else { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read Body element- unknown body type"); return false; } vl.append(reader.readElementText()); if (reader.error() != QXmlStreamReader::NoError) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid content.") .arg(QStringLiteral("Body")); return false; } vl.append(isHtml); val = vl; return true; } bool EwsItemPrivate::messageHeadersReader(QXmlStreamReader &reader, QVariant &val) { EwsItem::HeaderMap map = val.value(); while (reader.readNextStartElement()) { if (reader.namespaceUri() != ewsTypeNsUri) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected namespace in InternetMessageHeaders element:") << reader.namespaceUri(); return false; } if (reader.name() == QLatin1String("InternetMessageHeader")) { QStringRef nameRef = reader.attributes().value(QStringLiteral("HeaderName")); if (nameRef.isNull()) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Missing HeaderName attribute in InternetMessageHeader element."); return false; } QString name = nameRef.toString(); QString value = reader.readElementText(); map.insert(name, value); if (reader.error() != QXmlStreamReader::NoError) { qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read EWS request - invalid %1 element.") .arg(QStringLiteral("InternetMessageHeader")); return false; } } } val = QVariant::fromValue(map); return true; } bool EwsItemPrivate::recipientsReader(QXmlStreamReader &reader, QVariant &val) { EwsMailbox::List mboxList; while (reader.readNextStartElement()) { if (reader.namespaceUri() != ewsTypeNsUri) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected namespace in %1 element:").arg(reader.name().toString()) << reader.namespaceUri(); return false; } EwsMailbox mbox(reader); if (!mbox.isValid()) { return false; } mboxList.append(mbox); } val = QVariant::fromValue(mboxList); return true; } bool EwsItemPrivate::mailboxReader(QXmlStreamReader &reader, QVariant &val) { if (!reader.readNextStartElement()) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Expected mailbox in %1 element:").arg(reader.name().toString()); return false; } if (reader.namespaceUri() != ewsTypeNsUri) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected namespace in %1 element:").arg(reader.name().toString()) << reader.namespaceUri(); reader.skipCurrentElement(); return false; } EwsMailbox mbox(reader); if (!mbox.isValid()) { reader.skipCurrentElement(); return false; } val = QVariant::fromValue(mbox); // Leave the element reader.skipCurrentElement(); return true; } bool EwsItemPrivate::timezoneReader(QXmlStreamReader &reader, QVariant &val) { // TODO: This only reads the timezone identifier. QStringRef idRef = reader.attributes().value(QStringLiteral("Id")); if (idRef.isNull()) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading %1 element - missing %2 attribute") - .arg(reader.name().toString()).arg(QStringLiteral("Id")); + .arg(reader.name().toString(), QStringLiteral("Id")); reader.skipCurrentElement(); return false; } else { reader.skipCurrentElement(); val = idRef.toString(); return true; } } bool EwsItemPrivate::attendeesReader(QXmlStreamReader &reader, QVariant &val) { EwsAttendee::List attList; while (reader.readNextStartElement()) { if (reader.namespaceUri() != ewsTypeNsUri) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected namespace in %1 element:").arg(reader.name().toString()) << reader.namespaceUri(); return false; } EwsAttendee att(reader); if (!att.isValid()) { return false; } attList.append(att); } val = QVariant::fromValue(attList); return true; } bool EwsItemPrivate::occurrenceReader(QXmlStreamReader &reader, QVariant &val) { EwsOccurrence occ(reader); if (!occ.isValid()) { return false; } val = QVariant::fromValue(occ); return true; } bool EwsItemPrivate::occurrencesReader(QXmlStreamReader &reader, QVariant &val) { EwsOccurrence::List occList; while (reader.readNextStartElement()) { if (reader.namespaceUri() != ewsTypeNsUri) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected namespace in %1 element:") .arg(reader.name().toString()) << reader.namespaceUri(); return false; } EwsOccurrence occ(reader); if (!occ.isValid()) { return false; } occList.append(occ); } val = QVariant::fromValue(occList); return true; } bool EwsItemPrivate::categoriesReader(QXmlStreamReader &reader, QVariant &val) { QStringList categories; while (reader.readNextStartElement()) { if (reader.namespaceUri() != ewsTypeNsUri) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected namespace in %1 element:") .arg(reader.name().toString()) << reader.namespaceUri(); return false; } if (reader.name() == QLatin1String("String")) { categories.append(reader.readElementText()); if (reader.error() != QXmlStreamReader::NoError) { qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read EWS request - invalid %1 element.") .arg(QStringLiteral("Categories/Value")); return false; } } } val = categories; return true; } bool EwsItemPrivate::categoriesWriter(QXmlStreamWriter &writer, const QVariant &val) { const QStringList categories = val.toStringList(); for (const QString &cat : categories) { writer.writeTextElement(ewsTypeNsUri, QStringLiteral("String"), cat); } return true; } bool EwsItemPrivate::recurrenceReader(QXmlStreamReader &reader, QVariant &val) { EwsRecurrence recurrence(reader); val = QVariant::fromValue(recurrence); return true; } bool EwsItemPrivate::attachmentsReader(QXmlStreamReader &reader, QVariant &val) { EwsAttachment::List attList; while (reader.readNextStartElement()) { if (reader.namespaceUri() != ewsTypeNsUri) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected namespace in %1 element:").arg(reader.name().toString()) << reader.namespaceUri(); return false; } EwsAttachment att(reader); if (!att.isValid()) { return false; } attList.append(att); } val = QVariant::fromValue(attList); return true; } bool EwsItemPrivate::operator==(const EwsItemPrivate &other) const { if (!EwsItemBasePrivate::operator==(other)) { return false; } return mType == other.mType; } EwsItem::EwsItem() : EwsItemBase(QSharedDataPointer(new EwsItemPrivate())) { } EwsItem::EwsItem(QXmlStreamReader &reader) : EwsItemBase(QSharedDataPointer(new EwsItemPrivate())) { qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); EwsItemPrivate *d = reinterpret_cast(this->d.data()); // Check what item type are we const QStringRef elmName = reader.name(); if (elmName == QLatin1String("Item")) { d->mType = EwsItemTypeItem; const QStringRef subtype = reader.attributes().value(QStringLiteral("xsi:type")); if (!subtype.isEmpty()) { auto tokens = subtype.split(QLatin1Char(':')); const QStringRef type = tokens.size() == 1 ? tokens[0] : tokens[1]; if (type == QLatin1String("AbchPersonItemType")) { d->mType = EwsItemTypeAbchPerson; } } } else if (elmName == QLatin1String("Message")) { d->mType = EwsItemTypeMessage; } else if (elmName == QLatin1String("CalendarItem")) { d->mType = EwsItemTypeCalendarItem; } else if (elmName == QLatin1String("Contact")) { d->mType = EwsItemTypeContact; } else if (elmName == QLatin1String("DistributionList")) { d->mType = EwsItemTypeDistributionList; } else if (elmName == QLatin1String("MeetingMessage")) { d->mType = EwsItemTypeMeetingMessage; } else if (elmName == QLatin1String("MeetingRequest")) { d->mType = EwsItemTypeMeetingRequest; } else if (elmName == QLatin1String("MeetingResponse")) { d->mType = EwsItemTypeMeetingResponse; } else if (elmName == QLatin1String("MeetingCancellation")) { d->mType = EwsItemTypeMeetingCancellation; } else if (elmName == QLatin1String("PostItem")) { d->mType = EwsItemTypePostItem; } else if (elmName == QLatin1String("Task")) { d->mType = EwsItemTypeTask; } while (reader.readNextStartElement()) { if (reader.namespaceUri() != ewsTypeNsUri) { qCWarningNC(EWSCLI_LOG) << "Unexpected namespace in Item element:" << reader.namespaceUri(); return; } if (!readBaseItemElement(reader)) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Invalid Item child: %1").arg(reader.qualifiedName().toString()); return; } } d->mValid = true; } EwsItem::EwsItem(const EwsItem &other) : EwsItemBase(other.d) { } EwsItem::EwsItem(EwsItem &&other) : EwsItemBase(other.d) { } EwsItem::~EwsItem() { } EwsItem &EwsItem::operator=(const EwsItem &other) { d = other.d; return *this; } EwsItem &EwsItem::operator=(EwsItem &&other) { d = std::move(other.d); return *this; } EwsItemType EwsItem::type() const { const EwsItemPrivate *d = reinterpret_cast(this->d.data()); return d->mType; } void EwsItem::setType(EwsItemType type) { D_PTR d->mType = type; d->mValid = true; } EwsItemType EwsItem::internalType() const { D_CPTR EwsItemType type = d->mType; switch (type) { case EwsItemTypeMeetingMessage: case EwsItemTypeMeetingRequest: case EwsItemTypeMeetingResponse: case EwsItemTypeMeetingCancellation: type = EwsItemTypeMessage; break; case EwsItemTypeDistributionList: type = EwsItemTypeContact; break; default: break; } return type; } bool EwsItem::readBaseItemElement(QXmlStreamReader &reader) { D_PTR if (!d->mEwsXml.readItem(reader, QStringLiteral("Item"), ewsTypeNsUri)) { return false; } d->mFields = d->mEwsXml.values(); // The body item is special as it hold two values in one. Need to separate them into their // proper places. if (d->mFields.contains(EwsItemFieldBody)) { QVariantList vl = d->mFields[EwsItemFieldBody].value(); QVariantList::const_iterator it = vl.cbegin(); d->mFields[EwsItemFieldBody] = *it++; d->mFields[EwsItemFieldBodyIsHtml] = *it; } return true; } bool EwsItem::write(QXmlStreamWriter &writer) const { D_CPTR writer.writeStartElement(ewsTypeNsUri, ewsItemTypeNames[d->mType]); bool status = d->mEwsXml.writeItems(writer, ewsItemTypeNames[d->mType], ewsTypeNsUri, d->mFields); writer.writeEndElement(); return status; } bool EwsItem::operator==(const EwsItem &other) const { return *d == *other.d; } diff --git a/resources/ews/ewsclient/ewsmoveitemrequest.cpp b/resources/ews/ewsclient/ewsmoveitemrequest.cpp index 3611c8a36..9ce9dcefd 100644 --- a/resources/ews/ewsclient/ewsmoveitemrequest.cpp +++ b/resources/ews/ewsclient/ewsmoveitemrequest.cpp @@ -1,121 +1,121 @@ /* 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 "ewsmoveitemrequest.h" #include "ewsclient_debug.h" EwsMoveItemRequest::EwsMoveItemRequest(EwsClient &client, QObject *parent) : EwsRequest(client, parent) { } EwsMoveItemRequest::~EwsMoveItemRequest() { } void EwsMoveItemRequest::start() { QString reqString; QXmlStreamWriter writer(&reqString); startSoapDocument(writer); writer.writeStartElement(ewsMsgNsUri, QStringLiteral("MoveItem")); writer.writeStartElement(ewsMsgNsUri, QStringLiteral("ToFolderId")); mDestFolderId.writeFolderIds(writer); writer.writeEndElement(); writer.writeStartElement(ewsMsgNsUri, QStringLiteral("ItemIds")); for (const EwsId &id : qAsConst(mIds)) { id.writeItemIds(writer); } writer.writeEndElement(); writer.writeEndElement(); endSoapDocument(writer); qCDebugNCS(EWSCLI_REQUEST_LOG) << QStringLiteral("Starting MoveItem request (") << mIds << "to" << mDestFolderId << ")"; qCDebug(EWSCLI_PROTO_LOG) << reqString; prepare(reqString); doSend(); } bool EwsMoveItemRequest::parseResult(QXmlStreamReader &reader) { return parseResponseMessage(reader, QStringLiteral("MoveItem"), [this](QXmlStreamReader &reader) { return parseItemsResponse(reader); }); } bool EwsMoveItemRequest::parseItemsResponse(QXmlStreamReader &reader) { Response resp(reader); if (resp.responseClass() == EwsResponseUnknown) { return false; } if (EWSCLI_REQUEST_LOG().isDebugEnabled()) { if (resp.isSuccess()) { qCDebugNC(EWSCLI_REQUEST_LOG) << QStringLiteral("Got MoveItem response - OK"); } else { qCDebugNC(EWSCLI_REQUEST_LOG) << QStringLiteral("Got MoveItem response - %1") .arg(resp.responseMessage()); } } mResponses.append(resp); return true; } EwsMoveItemRequest::Response::Response(QXmlStreamReader &reader) : EwsRequest::Response(reader) { if (mClass == EwsResponseParseError) { return; } while (reader.readNextStartElement()) { if (reader.namespaceUri() != ewsMsgNsUri && reader.namespaceUri() != ewsTypeNsUri) { setErrorMsg(QStringLiteral("Unexpected namespace in %1 element: %2") - .arg(QStringLiteral("ResponseMessage")).arg(reader.namespaceUri().toString())); + .arg(QStringLiteral("ResponseMessage"), reader.namespaceUri().toString())); return; } if (reader.name() == QLatin1String("Items")) { if (reader.readNextStartElement()) { EwsItem item(reader); if (!item.isValid()) { return; } mId = item[EwsItemFieldItemId].value(); // Finish the Items element. reader.skipCurrentElement(); } } else if (!readResponseElement(reader)) { setErrorMsg(QStringLiteral("Failed to read EWS request - invalid response element.")); return; } } } diff --git a/resources/ews/ewsclient/ewsoccurrence.cpp b/resources/ews/ewsclient/ewsoccurrence.cpp index 529cea05b..aca69f4ab 100644 --- a/resources/ews/ewsclient/ewsoccurrence.cpp +++ b/resources/ews/ewsclient/ewsoccurrence.cpp @@ -1,150 +1,150 @@ /* Copyright (C) 2015-2017 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 "ewsoccurrence.h" #include #include #include "ewsclient_debug.h" #include "ewsid.h" #include "ewstypes.h" class EwsOccurrencePrivate : public QSharedData { public: EwsOccurrencePrivate(); virtual ~EwsOccurrencePrivate(); bool mValid; EwsId mItemId; QDateTime mStart; QDateTime mEnd; QDateTime mOriginalStart; }; EwsOccurrencePrivate::EwsOccurrencePrivate() : mValid(false) { } EwsOccurrence::EwsOccurrence() : d(new EwsOccurrencePrivate()) { } EwsOccurrence::EwsOccurrence(QXmlStreamReader &reader) : d(new EwsOccurrencePrivate()) { while (reader.readNextStartElement()) { if (reader.namespaceUri() != ewsTypeNsUri) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected namespace in mailbox element:") << reader.namespaceUri(); return; } if (reader.name() == QLatin1String("ItemId")) { d->mItemId = EwsId(reader); reader.skipCurrentElement(); } else if (reader.name() == QLatin1String("Start")) { d->mStart = QDateTime::fromString(reader.readElementText(), Qt::ISODate); if (reader.error() != QXmlStreamReader::NoError || !d->mStart.isValid()) { qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid %2 element.") - .arg(QStringLiteral("Occurrence")).arg(QStringLiteral("Start")); + .arg(QStringLiteral("Occurrence"), QStringLiteral("Start")); return; } } else if (reader.name() == QLatin1String("End")) { d->mEnd = QDateTime::fromString(reader.readElementText(), Qt::ISODate); if (reader.error() != QXmlStreamReader::NoError || !d->mStart.isValid()) { qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid %2 element.") - .arg(QStringLiteral("Occurrence")).arg(QStringLiteral("End")); + .arg(QStringLiteral("Occurrence"), QStringLiteral("End")); return; } } else if (reader.name() == QLatin1String("OriginalStart")) { d->mStart = QDateTime::fromString(reader.readElementText(), Qt::ISODate); if (reader.error() != QXmlStreamReader::NoError || !d->mStart.isValid()) { qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid %2 element.") - .arg(QStringLiteral("Occurrence")).arg(QStringLiteral("OriginalStart")); + .arg(QStringLiteral("Occurrence"), QStringLiteral("OriginalStart")); return; } } else { qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - unknown element: %2.") - .arg(QStringLiteral("Occurrence")).arg(reader.name().toString()); + .arg(QStringLiteral("Occurrence"), reader.name().toString()); return; } } d->mValid = true; } EwsOccurrencePrivate::~EwsOccurrencePrivate() { } EwsOccurrence::EwsOccurrence(const EwsOccurrence &other) : d(other.d) { } EwsOccurrence::EwsOccurrence(EwsOccurrence &&other) : d(std::move(other.d)) { } EwsOccurrence::~EwsOccurrence() { } EwsOccurrence &EwsOccurrence::operator=(const EwsOccurrence &other) { d = other.d; return *this; } EwsOccurrence &EwsOccurrence::operator=(EwsOccurrence &&other) { d = std::move(other.d); return *this; } bool EwsOccurrence::isValid() const { return d->mValid; } const EwsId &EwsOccurrence::itemId() const { return d->mItemId; } QDateTime EwsOccurrence::start() const { return d->mStart; } QDateTime EwsOccurrence::end() const { return d->mEnd; } QDateTime EwsOccurrence::originalStart() const { return d->mOriginalStart; } diff --git a/resources/ews/ewsclient/ewspoxautodiscoverrequest.cpp b/resources/ews/ewsclient/ewspoxautodiscoverrequest.cpp index 0c44fd00e..332772fb1 100644 --- a/resources/ews/ewsclient/ewspoxautodiscoverrequest.cpp +++ b/resources/ews/ewsclient/ewspoxautodiscoverrequest.cpp @@ -1,288 +1,288 @@ /* Copyright (C) 2015-2017 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 "ewspoxautodiscoverrequest.h" #include #include #include #include #include "ewsclient_debug.h" static const QString poxAdOuReqNsUri = QStringLiteral("http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006"); static const QString poxAdRespNsUri = QStringLiteral("http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006"); static const QString poxAdOuRespNsUri = QStringLiteral("http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a"); EwsPoxAutodiscoverRequest::EwsPoxAutodiscoverRequest(const QUrl &url, const QString &email, const QString &userAgent, bool useNTLMv2, QObject *parent) : EwsJob(parent) , mUrl(url) , mEmail(email) , mUserAgent(userAgent) , mUseNTLMv2(useNTLMv2) , mServerVersion(EwsServerVersion::ewsVersion2007Sp1) , mAction(Settings) { } EwsPoxAutodiscoverRequest::~EwsPoxAutodiscoverRequest() { } void EwsPoxAutodiscoverRequest::doSend() { Q_FOREACH (KJob *job, subjobs()) { job->start(); } } void EwsPoxAutodiscoverRequest::prepare(const QString &body) { mBody = body; mLastUrl = mUrl; KIO::TransferJob *job = KIO::http_post(mUrl, body.toUtf8(), KIO::HideProgressInfo); job->addMetaData(QStringLiteral("content-type"), QStringLiteral("text/xml")); job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true")); if (mUseNTLMv2) { job->addMetaData(QStringLiteral("EnableNTLMv2Auth"), QStringLiteral("true")); } if (!mUserAgent.isEmpty()) { job->addMetaData(QStringLiteral("UserAgent"), mUserAgent); } //config->readEntry("no-spoof-check", false) connect(job, &KIO::TransferJob::result, this, &EwsPoxAutodiscoverRequest::requestResult); connect(job, &KIO::TransferJob::data, this, &EwsPoxAutodiscoverRequest::requestData); connect(job, &KIO::TransferJob::redirection, this, &EwsPoxAutodiscoverRequest::requestRedirect); addSubjob(job); } void EwsPoxAutodiscoverRequest::start() { QString reqString; QXmlStreamWriter writer(&reqString); writer.writeStartDocument(); writer.writeDefaultNamespace(poxAdOuReqNsUri); writer.writeStartElement(poxAdOuReqNsUri, QStringLiteral("Autodiscover")); writer.writeStartElement(poxAdOuReqNsUri, QStringLiteral("Request")); writer.writeTextElement(poxAdOuReqNsUri, QStringLiteral("EMailAddress"), mEmail); writer.writeTextElement(poxAdOuReqNsUri, QStringLiteral("AcceptableResponseSchema"), poxAdOuRespNsUri); writer.writeEndElement(); // Request writer.writeEndElement(); // Autodiscover writer.writeEndDocument(); qCDebug(EWSCLI_PROTO_LOG) << reqString; qCDebugNC(EWSCLI_REQUEST_LOG) << QStringLiteral("Starting POX Autodiscovery request (url: ") << mUrl << QStringLiteral(", email: ") << mEmail; prepare(reqString); doSend(); } void EwsPoxAutodiscoverRequest::requestData(KIO::Job *job, const QByteArray &data) { Q_UNUSED(job); qCDebug(EWSCLI_PROTO_LOG) << "data" << job << data; mResponseData += QString::fromUtf8(data); } void EwsPoxAutodiscoverRequest::requestResult(KJob *job) { if (EWSCLI_PROTO_LOG().isDebugEnabled()) { ewsLogDir.setAutoRemove(false); if (ewsLogDir.isValid()) { QTemporaryFile dumpFile(ewsLogDir.path() + QStringLiteral("/ews_xmldump_XXXXXXX.xml")); dumpFile.open(); dumpFile.setAutoRemove(false); dumpFile.write(mResponseData.toUtf8()); qCDebug(EWSCLI_PROTO_LOG) << "response dumped to" << dumpFile.fileName(); dumpFile.close(); } } KIO::TransferJob *trJob = qobject_cast(job); int resp = trJob->metaData()[QStringLiteral("responsecode")].toUInt(); if (job->error() != 0) { setErrorMsg(QStringLiteral("Failed to process EWS request: ") + job->errorString()); setError(job->error()); } else if (resp >= 300) { setErrorMsg(QStringLiteral("Failed to process EWS request - HTTP code %1").arg(resp)); setError(resp); } else { QXmlStreamReader reader(mResponseData); readResponse(reader); } emitResult(); } bool EwsPoxAutodiscoverRequest::readResponse(QXmlStreamReader &reader) { if (!reader.readNextStartElement()) { return setErrorMsg(QStringLiteral("Failed to read POX response XML")); } if ((reader.name() != QLatin1String("Autodiscover")) || (reader.namespaceUri() != poxAdRespNsUri)) { return setErrorMsg(QStringLiteral("Failed to read POX response - not an Autodiscover response")); } if (!reader.readNextStartElement()) { return setErrorMsg(QStringLiteral("Failed to read POX response - expected %1 element") .arg(QStringLiteral("Response"))); } if ((reader.name() != QLatin1String("Response")) || (reader.namespaceUri() != poxAdOuRespNsUri)) { return setErrorMsg(QStringLiteral("Failed to read POX response - expected %1 element, found %2") .arg(QStringLiteral("Response").arg(reader.name().toString()))); } while (reader.readNextStartElement()) { if (reader.namespaceUri() != poxAdOuRespNsUri) { return setErrorMsg(QStringLiteral("Failed to read POX response - invalid namespace")); } if (reader.name() == QLatin1String("User")) { reader.skipCurrentElement(); } else if (reader.name() == QLatin1String("Account")) { if (!readAccount(reader)) { return false; } } else { return setErrorMsg(QStringLiteral("Failed to read POX response - unknown element '%1' inside '%2'") - .arg(reader.name().toString()).arg(QStringLiteral("Response"))); + .arg(reader.name().toString(), QStringLiteral("Response"))); } } return true; } bool EwsPoxAutodiscoverRequest::readAccount(QXmlStreamReader &reader) { while (reader.readNextStartElement()) { if (reader.namespaceUri() != poxAdOuRespNsUri) { return setErrorMsg(QStringLiteral("Failed to read POX response - invalid namespace")); } const QStringRef readerName = reader.name(); if (readerName == QLatin1String("Action")) { QString action = reader.readElementText(); if (action == QLatin1String("settings")) { mAction = Settings; } else if (action == QLatin1String("redirectUrl")) { mAction = RedirectUrl; } else if (action == QLatin1String("redirectAddr")) { mAction = RedirectAddr; } else { return setErrorMsg(QStringLiteral("Failed to read POX response - unknown action '%1'") .arg(action)); } } else if (readerName == QLatin1String("RedirectUrl")) { mRedirectUrl = reader.readElementText(); } else if (readerName == QLatin1String("RedirectAddr")) { mRedirectAddr = reader.readElementText(); } else if (readerName == QLatin1String("RedirectAddr")) { mRedirectAddr = reader.readElementText(); } else if (readerName == QLatin1String("Protocol")) { if (!readProtocol(reader)) { return false; } } else { reader.skipCurrentElement(); } } return true; } bool EwsPoxAutodiscoverRequest::readProtocol(QXmlStreamReader &reader) { Protocol proto; while (reader.readNextStartElement()) { if (reader.namespaceUri() != poxAdOuRespNsUri) { return setErrorMsg(QStringLiteral("Failed to read POX response - invalid namespace")); } const QStringRef readerName = reader.name(); if (readerName == QLatin1String("Type")) { QString type = reader.readElementText(); if (type == QLatin1String("EXCH")) { proto.mType = ExchangeProto; } else if (type == QLatin1String("EXPR")) { proto.mType = ExchangeProxyProto; } else if (type == QLatin1String("WEB")) { proto.mType = ExchangeWebProto; } else { return setErrorMsg(QStringLiteral("Failed to read POX response - unknown protocol '%1'") .arg(type)); } } else if (readerName == QLatin1String("EwsUrl")) { proto.mEwsUrl = reader.readElementText(); } else if (readerName == QLatin1String("OabUrl")) { proto.mOabUrl = reader.readElementText(); } else { reader.skipCurrentElement(); } } qCDebug(EWSCLI_LOG) << "Adding proto type" << proto.mType << proto.isValid(); mProtocols[proto.mType] = proto; return true; } void EwsPoxAutodiscoverRequest::requestRedirect(KIO::Job *job, const QUrl &url) { Q_UNUSED(job); qCDebugNC(EWSCLI_REQUEST_LOG) << QStringLiteral("Got HTTP redirect to: ") << mUrl; mLastUrl = url; } void EwsPoxAutodiscoverRequest::dump() const { ewsLogDir.setAutoRemove(false); if (ewsLogDir.isValid()) { QTemporaryFile reqDumpFile(ewsLogDir.path() + QStringLiteral("/ews_xmlreqdump_XXXXXXX.xml")); reqDumpFile.open(); reqDumpFile.setAutoRemove(false); reqDumpFile.write(mBody.toUtf8()); reqDumpFile.close(); QTemporaryFile resDumpFile(ewsLogDir.path() + QStringLiteral("/ews_xmlresdump_XXXXXXX.xml")); resDumpFile.open(); resDumpFile.setAutoRemove(false); resDumpFile.write(mResponseData.toUtf8()); resDumpFile.close(); qCDebug(EWSCLI_LOG) << "request dumped to" << reqDumpFile.fileName(); qCDebug(EWSCLI_LOG) << "response dumped to" << resDumpFile.fileName(); } else { qCWarning(EWSCLI_LOG) << "failed to dump request and response"; } } diff --git a/resources/ews/ewsclient/ewsrecurrence.cpp b/resources/ews/ewsclient/ewsrecurrence.cpp index 56ba027be..47704f698 100644 --- a/resources/ews/ewsclient/ewsrecurrence.cpp +++ b/resources/ews/ewsclient/ewsrecurrence.cpp @@ -1,565 +1,565 @@ /* Copyright (C) 2015-2017 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 "ewsrecurrence.h" #include #include #include #include "ewsclient_debug.h" #include "ewstypes.h" using namespace KCalendarCore; static const QString dayOfWeekNames[] = { QStringLiteral("Monday"), QStringLiteral("Tuesday"), QStringLiteral("Wednesday"), QStringLiteral("Thursday"), QStringLiteral("Friday"), QStringLiteral("Saturday"), QStringLiteral("Sunday"), QStringLiteral("Day"), QStringLiteral("Weekday"), QStringLiteral("WeekendDay"), }; Q_CONSTEXPR unsigned dayOfWeekNameCount = sizeof(dayOfWeekNames) / sizeof(dayOfWeekNames[0]); static const QString dayOfWeekIndexNames[] = { QStringLiteral("First"), QStringLiteral("Second"), QStringLiteral("Third"), QStringLiteral("Fourth"), QStringLiteral("Last") }; Q_CONSTEXPR unsigned dayOfWeekIndexNameCount = sizeof(dayOfWeekIndexNames) / sizeof(dayOfWeekIndexNames[0]); static const QString monthNames[] = { QStringLiteral("January"), QStringLiteral("February"), QStringLiteral("March"), QStringLiteral("April"), QStringLiteral("May"), QStringLiteral("June"), QStringLiteral("July"), QStringLiteral("August"), QStringLiteral("September"), QStringLiteral("October"), QStringLiteral("November"), QStringLiteral("December") }; Q_CONSTEXPR unsigned monthNameCount = sizeof(monthNames) / sizeof(monthNames[0]); EwsRecurrence::EwsRecurrence() : Recurrence() { } EwsRecurrence::EwsRecurrence(QXmlStreamReader &reader) { while (reader.readNextStartElement()) { QString elmName = reader.name().toString(); if (reader.namespaceUri() != ewsTypeNsUri) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected namespace in %1 element:").arg(elmName) << reader.namespaceUri(); return; } if (elmName == QLatin1String("RelativeYearlyRecurrence")) { if (!readRelativeYearlyRecurrence(reader)) { return; } } else if (elmName == QLatin1String("AbsoluteYearlyRecurrence")) { if (!readAbsoluteYearlyRecurrence(reader)) { return; } } else if (elmName == QLatin1String("RelativeMonthlyRecurrence")) { if (!readRelativeMonthlyRecurrence(reader)) { return; } } else if (elmName == QLatin1String("AbsoluteMonthlyRecurrence")) { if (!readAbsoluteMonthlyRecurrence(reader)) { return; } } else if (elmName == QLatin1String("WeeklyRecurrence")) { if (!readWeeklyRecurrence(reader)) { return; } } else if (elmName == QLatin1String("DailyRecurrence")) { if (!readWeeklyRecurrence(reader)) { return; } } else if (elmName == QLatin1String("NoEndRecurrence")) { // Ignore - this is the default reader.skipCurrentElement(); } else if (elmName == QLatin1String("EndDateRecurrence")) { if (!readEndDateRecurrence(reader)) { return; } } else if (elmName == QLatin1String("NumberedRecurrence")) { if (!readNumberedRecurrence(reader)) { return; } } else { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - unknown element: %2.") - .arg(QStringLiteral("Recurrence")).arg(elmName); + .arg(QStringLiteral("Recurrence"), elmName); return; } } } EwsRecurrence::EwsRecurrence(const EwsRecurrence &other) : Recurrence(other) { } bool EwsRecurrence::readRelativeYearlyRecurrence(QXmlStreamReader &reader) { QBitArray dow(7); short dowWeekIndex = 0; short month = 0; bool hasDow = false; bool hasDowWeekIndex = false; bool hasMonth = false; while (reader.readNextStartElement()) { QString elmName = reader.name().toString(); if (reader.namespaceUri() != ewsTypeNsUri) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected namespace in %1 element:").arg(elmName) << reader.namespaceUri(); return false; } if (elmName == QLatin1String("DaysOfWeek")) { if (!readDow(reader, dow)) { return false; } hasDow = true; } else if (elmName == QLatin1String("DayOfWeekIndex")) { bool ok; QString text = reader.readElementText(); dowWeekIndex = decodeEnumString(text, dayOfWeekIndexNames, dayOfWeekIndexNameCount, &ok); if (reader.error() != QXmlStreamReader::NoError || !ok) { qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read EWS request - invalid %1 element (value: %2).") .arg(QStringLiteral("DayOfWeekIndex").arg(text)); return false; } if (dowWeekIndex == 4) { // "Last" dowWeekIndex = -1; } hasDowWeekIndex = true; } else if (elmName == QLatin1String("Month")) { bool ok; QString text = reader.readElementText(); month = decodeEnumString(text, monthNames, monthNameCount, &ok); if (reader.error() != QXmlStreamReader::NoError || !ok) { qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read EWS request - invalid %1 element (value: %2).") - .arg(QStringLiteral("Month").arg(text)); + .arg(QStringLiteral("Month"), text); return false; } hasMonth = true; } else { qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - unknown element: %2.") - .arg(QStringLiteral("RelativeYearlyRecurrence")).arg(elmName); + .arg(QStringLiteral("RelativeYearlyRecurrence"), elmName); return false; } } if (!hasMonth || !hasDow || !hasDowWeekIndex) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read Recurrence element - expected all of Month, DaysOfWeek and DayOfWeekIndex elements."); return false; } addYearlyMonth(month); addYearlyPos(dowWeekIndex, dow); return true; } bool EwsRecurrence::readAbsoluteYearlyRecurrence(QXmlStreamReader &reader) { short dom = 0; short month = 0; bool hasDom = false; bool hasMonth = false; while (reader.readNextStartElement()) { QString elmName = reader.name().toString(); if (reader.namespaceUri() != ewsTypeNsUri) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected namespace in %1 element:").arg(elmName) << reader.namespaceUri(); return false; } if (elmName == QLatin1String("DayOfMonth")) { bool ok; QString text = reader.readElementText(); dom = text.toShort(&ok); if (reader.error() != QXmlStreamReader::NoError || !ok) { qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read EWS request - invalid %1 element (value: %2).") .arg(QStringLiteral("DayOfMonth").arg(text)); return false; } hasDom = true; } else if (elmName == QLatin1String("Month")) { bool ok; QString text = reader.readElementText(); month = decodeEnumString(text, monthNames, monthNameCount, &ok); if (reader.error() != QXmlStreamReader::NoError || !ok) { qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read EWS request - invalid %1 element (value: %2).") .arg(QStringLiteral("Month").arg(text)); return false; } hasMonth = true; } else { qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read recurrence element - unknown element: %1.") .arg(elmName); return false; } } if (!hasDom || !hasMonth) { qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read recurrence element - need both month and dom values."); return false; } // "If for a particular month this value is larger than the number of days in the month, // the last day of the month is assumed for this property." QDate date(2000, month, 1); if (dom > date.daysInMonth()) { dom = -1; } addYearlyMonth(month); addYearlyDay(dom); return true; } bool EwsRecurrence::readRelativeMonthlyRecurrence(QXmlStreamReader &reader) { QBitArray dow(7); short dowWeekIndex = 0; int interval = 0; bool hasDow = false; bool hasDowWeekIndex = false; bool hasInterval = false; while (reader.readNextStartElement()) { QString elmName = reader.name().toString(); if (reader.namespaceUri() != ewsTypeNsUri) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected namespace in %1 element:").arg(elmName) << reader.namespaceUri(); return false; } if (elmName == QLatin1String("Interval")) { bool ok; QString text = reader.readElementText(); interval = text.toInt(&ok); if (reader.error() != QXmlStreamReader::NoError || !ok) { qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read EWS request - invalid %1 element (value: %2).") .arg(QStringLiteral("Interval").arg(text)); return false; } hasInterval = true; } else if (elmName == QLatin1String("DaysOfWeek")) { if (!readDow(reader, dow)) { return false; } hasDow = true; } else if (elmName == QLatin1String("DayOfWeekIndex")) { bool ok; QString text = reader.readElementText(); dowWeekIndex = decodeEnumString(text, dayOfWeekIndexNames, dayOfWeekIndexNameCount, &ok); if (reader.error() != QXmlStreamReader::NoError || !ok) { qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read EWS request - invalid %1 element (value: %2).") .arg(QStringLiteral("DayOfWeekIndex").arg(text)); return false; } if (dowWeekIndex == 4) { // "Last" dowWeekIndex = -1; } hasDowWeekIndex = true; } else { qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read Recurrence element - unknown element: %1.") .arg(elmName); return false; } } if (!hasInterval || !hasDow || !hasDowWeekIndex) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read Recurrence element - expected all of Interval, DaysOfWeek and DayOfWeekIndex elements."); return false; } addMonthlyPos(dowWeekIndex, dow); setFrequency(interval); return true; } bool EwsRecurrence::readAbsoluteMonthlyRecurrence(QXmlStreamReader &reader) { short dom = 0; int interval = 0; bool hasInterval = false; bool hasDom = false; while (reader.readNextStartElement()) { QString elmName = reader.name().toString(); if (reader.namespaceUri() != ewsTypeNsUri) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected namespace in %1 element:").arg(elmName) << reader.namespaceUri(); return false; } if (elmName == QLatin1String("Interval")) { bool ok; QString text = reader.readElementText(); interval = text.toInt(&ok); if (reader.error() != QXmlStreamReader::NoError || !ok) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read EWS request - invalid %1 element (value: %2).") .arg(QStringLiteral("Interval").arg(text)); return false; } hasInterval = true; } else if (elmName == QLatin1String("DayOfMonth")) { bool ok; QString text = reader.readElementText(); dom = text.toShort(&ok); if (reader.error() != QXmlStreamReader::NoError || !ok) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read EWS request - invalid %1 element (value: %2).") .arg(QStringLiteral("DayOfMonth").arg(text)); return false; } hasDom = true; } else { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read Recurrence element - unknown element: %1.") .arg(elmName); return false; } } if (!hasInterval || !hasDom) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read Recurrence element - expected both Interval and DayOfMonth."); return false; } addMonthlyDate(dom); setFrequency(interval); return true; } bool EwsRecurrence::readWeeklyRecurrence(QXmlStreamReader &reader) { int interval = 1; QBitArray dow(7); int weekStart = 0; bool hasInterval = false; bool hasDow = false; bool hasWeekStart = false; while (reader.readNextStartElement()) { QString elmName = reader.name().toString(); if (reader.namespaceUri() != ewsTypeNsUri) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected namespace in %1 element:").arg(elmName) << reader.namespaceUri(); return false; } if (elmName == QLatin1String("Interval")) { bool ok; QString text = reader.readElementText(); interval = text.toInt(&ok); if (reader.error() != QXmlStreamReader::NoError || !ok) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read EWS request - invalid %1 element (value: %2).") .arg(QStringLiteral("Interval").arg(text)); return false; } hasInterval = true; } else if (elmName == QLatin1String("DaysOfWeek")) { if (!readDow(reader, dow)) { return false; } hasDow = true; } else if (elmName == QLatin1String("FirstDayOfWeek")) { bool ok; QString text = reader.readElementText(); weekStart = decodeEnumString(text, dayOfWeekNames, dayOfWeekNameCount, &ok) + 1; if (reader.error() != QXmlStreamReader::NoError || !ok) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read EWS request - invalid %1 element (value: %2).") .arg(QStringLiteral("FirstDayOfWeek").arg(text)); return false; } hasWeekStart = true; } else { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - unknown element: %2.") - .arg(QStringLiteral("WeeklyRecurrence")).arg(elmName); + .arg(QStringLiteral("WeeklyRecurrence"), elmName); return false; } } if (!hasInterval || !hasDow || !hasWeekStart) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read Recurrence element - expected all of Interval, DaysOfWeek and FirstDatOfWeek elements."); return false; } setWeekly(interval, dow, weekStart); return true; } bool EwsRecurrence::readDailyRecurrence(QXmlStreamReader &reader) { int interval = 1; bool hasInterval = false; while (reader.readNextStartElement()) { QString elmName = reader.name().toString(); if (reader.namespaceUri() != ewsTypeNsUri) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected namespace in %1 element:").arg(elmName) << reader.namespaceUri(); return false; } if (elmName == QLatin1String("Interval")) { bool ok; QString text = reader.readElementText(); interval = text.toInt(&ok); if (reader.error() != QXmlStreamReader::NoError || !ok) { qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read EWS request - invalid %1 element (value: %2).") .arg(QStringLiteral("Interval").arg(text)); return false; } hasInterval = true; } else { qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read recurrence element - unknown element: %1.") .arg(elmName); return false; } } if (!hasInterval) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read Recurrence element - expected an Interval element."); return false; } setDaily(interval); return true; } bool EwsRecurrence::readEndDateRecurrence(QXmlStreamReader &reader) { QDate dateEnd; while (reader.readNextStartElement()) { QString elmName = reader.name().toString(); if (reader.namespaceUri() != ewsTypeNsUri) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected namespace in %1 element:").arg(elmName) << reader.namespaceUri(); return false; } if (elmName == QLatin1String("EndDate")) { QString text = reader.readElementText(); dateEnd = QDate::fromString(text, Qt::ISODate); if (reader.error() != QXmlStreamReader::NoError || !dateEnd.isValid()) { qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read EWS request - invalid %1 element (value: %2).") .arg(QStringLiteral("EndDate").arg(text)); return false; } } else if (elmName == QLatin1String("StartDate")) { // Don't care reader.skipCurrentElement(); } else { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - unknown element: %2.") - .arg(QStringLiteral("EndDateRecurrence")).arg(elmName); + .arg(QStringLiteral("EndDateRecurrence"), elmName); return false; } } setEndDate(dateEnd); return true; } bool EwsRecurrence::readNumberedRecurrence(QXmlStreamReader &reader) { int numOccurrences = 0; while (reader.readNextStartElement()) { QString elmName = reader.name().toString(); if (reader.namespaceUri() != ewsTypeNsUri) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected namespace in %1 element:").arg(elmName) << reader.namespaceUri(); return false; } if (elmName == QLatin1String("NumberOfOccurrences")) { bool ok; QString text = reader.readElementText(); numOccurrences = text.toInt(&ok); if (reader.error() != QXmlStreamReader::NoError || !ok) { qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read EWS request - invalid %1 element (value: %2).") .arg(QStringLiteral("NumberOfOccurrences").arg(text)); return false; } } else if (elmName == QLatin1String("StartDate")) { // Don't care reader.skipCurrentElement(); } else { qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read recurrence element - unknown element: %1.") .arg(elmName); return false; } } setDuration(numOccurrences); return true; } bool EwsRecurrence::readDow(QXmlStreamReader &reader, QBitArray &dow) { bool ok; QString text = reader.readElementText(); const QStringList days = text.split(QLatin1Char(' ')); for (const QString &day : days) { short dowIndex = decodeEnumString(day, dayOfWeekNames, dayOfWeekNameCount, &ok); if (reader.error() != QXmlStreamReader::NoError || !ok) { qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read EWS request - invalid %1 element (value: %2).") .arg(QStringLiteral("DaysOfWeek").arg(day)); return false; } if (dowIndex == 7) { // "Day" dow.fill(true, 0, 7); } else if (dowIndex == 8) { // "Weekday" dow.fill(true, 0, 5); } else if (dowIndex == 9) { // "WeekendDay" dow.fill(true, 5, 7); } else { dow.setBit(dowIndex); } } return true; } diff --git a/resources/ews/ewsclient/ewsunsubscriberequest.cpp b/resources/ews/ewsclient/ewsunsubscriberequest.cpp index 0e40eaeea..292913a65 100644 --- a/resources/ews/ewsclient/ewsunsubscriberequest.cpp +++ b/resources/ews/ewsclient/ewsunsubscriberequest.cpp @@ -1,95 +1,95 @@ /* Copyright (C) 2015-2017 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 "ewsunsubscriberequest.h" #include #include "ewsclient_debug.h" EwsUnsubscribeRequest::EwsUnsubscribeRequest(EwsClient &client, QObject *parent) : EwsRequest(client, parent) { } EwsUnsubscribeRequest::~EwsUnsubscribeRequest() { } void EwsUnsubscribeRequest::start() { QString reqString; QXmlStreamWriter writer(&reqString); startSoapDocument(writer); writer.writeStartElement(ewsMsgNsUri, QStringLiteral("Unsubscribe")); writer.writeTextElement(ewsMsgNsUri, QStringLiteral("SubscriptionId"), mSubscriptionId); writer.writeEndElement(); // Unsubscribe endSoapDocument(writer); qCDebug(EWSCLI_PROTO_LOG) << reqString; prepare(reqString); doSend(); } bool EwsUnsubscribeRequest::parseResult(QXmlStreamReader &reader) { return parseResponseMessage(reader, QStringLiteral("Unsubscribe"), [this](QXmlStreamReader &reader) { return parseUnsubscribeResponse(reader); }); } bool EwsUnsubscribeRequest::parseUnsubscribeResponse(QXmlStreamReader &reader) { QSharedPointer resp(new Response(reader)); if (resp->responseClass() == EwsResponseUnknown) { return false; } mResponse = resp; return true; } EwsUnsubscribeRequest::Response::Response(QXmlStreamReader &reader) : EwsRequest::Response(reader) { if (mClass == EwsResponseParseError) { return; } while (reader.readNextStartElement()) { if (reader.namespaceUri() != ewsMsgNsUri && reader.namespaceUri() != ewsTypeNsUri) { setErrorMsg(QStringLiteral("Unexpected namespace in %1 element: %2") - .arg(QStringLiteral("ResponseMessage")).arg(reader.namespaceUri().toString())); + .arg(QStringLiteral("ResponseMessage"), reader.namespaceUri().toString())); return; } if (!readResponseElement(reader)) { setErrorMsg(QStringLiteral("Failed to read EWS request - invalid response element.")); return; } } } diff --git a/resources/ews/ewsclient/ewsupdateitemrequest.cpp b/resources/ews/ewsclient/ewsupdateitemrequest.cpp index 857e0dc66..fdc307213 100644 --- a/resources/ews/ewsclient/ewsupdateitemrequest.cpp +++ b/resources/ews/ewsclient/ewsupdateitemrequest.cpp @@ -1,231 +1,231 @@ /* 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 "ewsupdateitemrequest.h" #include "ewsclient_debug.h" static const QVector conflictResolutionNames = { QStringLiteral("NeverOverwrite"), QStringLiteral("AutoResolve"), QStringLiteral("AlwaysOverwrite") }; static const QVector messageDispositionNames = { QStringLiteral("SaveOnly"), QStringLiteral("SendOnly"), QStringLiteral("SendAndSaveCopy") }; static const QVector meetingDispositionNames = { QStringLiteral("SendToNone"), QStringLiteral("SendOnlyToAll"), QStringLiteral("SendOnlyToChanged"), QStringLiteral("SendToAllAndSaveCopy"), QStringLiteral("SendToChangedAndSaveCopy") }; static const QVector updateTypeElementNames = { QStringLiteral("AppendToItemField"), QStringLiteral("SetItemField"), QStringLiteral("DeleteItemField") }; EwsUpdateItemRequest::EwsUpdateItemRequest(EwsClient &client, QObject *parent) : EwsRequest(client, parent) , mMessageDisp(EwsDispSaveOnly) , mConflictResol(EwsResolAlwaysOverwrite) , mMeetingDisp(EwsMeetingDispUnspecified) { } EwsUpdateItemRequest::~EwsUpdateItemRequest() { } void EwsUpdateItemRequest::start() { QString reqString; QXmlStreamWriter writer(&reqString); startSoapDocument(writer); writer.writeStartElement(ewsMsgNsUri, QStringLiteral("UpdateItem")); writer.writeAttribute(QStringLiteral("ConflictResolution"), conflictResolutionNames[mConflictResol]); writer.writeAttribute(QStringLiteral("MessageDisposition"), messageDispositionNames[mMessageDisp]); if (mMeetingDisp != EwsMeetingDispUnspecified) { writer.writeAttribute(QStringLiteral("SendMeetingInvitationsOrCancellations"), meetingDispositionNames[mMeetingDisp]); } if (mSavedFolderId.type() != EwsId::Unspecified) { writer.writeStartElement(ewsMsgNsUri, QStringLiteral("SavedItemFolderId")); mSavedFolderId.writeFolderIds(writer); writer.writeEndElement(); } writer.writeStartElement(ewsMsgNsUri, QStringLiteral("ItemChanges")); for (const ItemChange &ch : qAsConst(mChanges)) { ch.write(writer); } writer.writeEndElement(); writer.writeEndElement(); endSoapDocument(writer); qCDebugNC(EWSCLI_REQUEST_LOG) << QStringLiteral("Starting UpdateItem request (%1 changes)") .arg(mChanges.size()); qCDebug(EWSCLI_PROTO_LOG) << reqString; prepare(reqString); doSend(); } bool EwsUpdateItemRequest::parseResult(QXmlStreamReader &reader) { return parseResponseMessage(reader, QStringLiteral("UpdateItem"), [this](QXmlStreamReader &reader) { return parseItemsResponse(reader); }); } bool EwsUpdateItemRequest::parseItemsResponse(QXmlStreamReader &reader) { Response resp(reader); if (resp.responseClass() == EwsResponseUnknown) { return false; } if (EWSCLI_REQUEST_LOG().isDebugEnabled()) { if (resp.isSuccess()) { qCDebugNC(EWSCLI_REQUEST_LOG) << QStringLiteral("Got UpdateItem response - OK"); } else { qCDebugNC(EWSCLI_REQUEST_LOG) << QStringLiteral("Got UpdateItem response - %1") .arg(resp.responseMessage()); } } mResponses.append(resp); return true; } EwsUpdateItemRequest::Response::Response(QXmlStreamReader &reader) : EwsRequest::Response(reader) , mConflictCount(0) { if (mClass == EwsResponseParseError) { return; } while (reader.readNextStartElement()) { if (reader.namespaceUri() != ewsMsgNsUri && reader.namespaceUri() != ewsTypeNsUri) { setErrorMsg(QStringLiteral("Unexpected namespace in %1 element: %2") - .arg(QStringLiteral("ResponseMessage")).arg(reader.namespaceUri().toString())); + .arg(QStringLiteral("ResponseMessage"), reader.namespaceUri().toString())); return; } if (reader.name() == QLatin1String("Items")) { if (reader.readNextStartElement()) { EwsItem item(reader); if (!item.isValid()) { return; } mId = item[EwsItemFieldItemId].value(); } // Finish the Items element. reader.skipCurrentElement(); } else if (reader.name() == QLatin1String("ConflictResults")) { if (!reader.readNextStartElement()) { setErrorMsg(QStringLiteral("Failed to read EWS request - expected a %1 element inside %2 element.") - .arg(QStringLiteral("Value")).arg(QStringLiteral("ConflictResults"))); + .arg(QStringLiteral("Value"), QStringLiteral("ConflictResults"))); return; } if (reader.name() != QLatin1String("Count")) { setErrorMsg(QStringLiteral("Failed to read EWS request - expected a %1 element inside %2 element.") - .arg(QStringLiteral("Count")).arg(QStringLiteral("ConflictResults"))); + .arg(QStringLiteral("Count"), QStringLiteral("ConflictResults"))); return; } bool ok; mConflictCount = reader.readElementText().toUInt(&ok); if (!ok) { setErrorMsg(QStringLiteral("Failed to read EWS request - invalid %1 element.") .arg(QStringLiteral("ConflictResults/Value"))); } // Finish the Value element. reader.skipCurrentElement(); } else if (!readResponseElement(reader)) { setErrorMsg(QStringLiteral("Failed to read EWS request - invalid response element %1.").arg(reader.name().toString())); return; } } } bool EwsUpdateItemRequest::Update::write(QXmlStreamWriter &writer, EwsItemType itemType) const { bool retVal = true; writer.writeStartElement(ewsTypeNsUri, updateTypeElementNames[mType]); mField.write(writer); if (mType != Delete) { writer.writeStartElement(ewsTypeNsUri, ewsItemTypeNames[itemType]); retVal = mField.writeWithValue(writer, mValue); writer.writeEndElement(); } writer.writeEndElement(); return retVal; } bool EwsUpdateItemRequest::ItemChange::write(QXmlStreamWriter &writer) const { bool retVal = true; writer.writeStartElement(ewsTypeNsUri, QStringLiteral("ItemChange")); mId.writeItemIds(writer); writer.writeStartElement(ewsTypeNsUri, QStringLiteral("Updates")); Q_FOREACH (const QSharedPointer &upd, mUpdates) { if (!upd->write(writer, mType)) { retVal = false; break; } } writer.writeEndElement(); writer.writeEndElement(); return retVal; } diff --git a/resources/ews/ewsclient/ewsxml.cpp b/resources/ews/ewsclient/ewsxml.cpp index 31506ac37..c89140827 100644 --- a/resources/ews/ewsclient/ewsxml.cpp +++ b/resources/ews/ewsclient/ewsxml.cpp @@ -1,379 +1,379 @@ /* Copyright (C) 2015-2017 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 "ewsxml.h" #include #include "ewsclient_debug.h" #include "ewsfolder.h" #include "ewsid.h" #include "ewsitem.h" static const QVector messageSensitivityNames = { QStringLiteral("Normal"), QStringLiteral("Personal"), QStringLiteral("Private"), QStringLiteral("Confidential") }; static const QVector messageImportanceNames = { QStringLiteral("Low"), QStringLiteral("Normal"), QStringLiteral("High") }; static const QVector calendarItemTypeNames = { QStringLiteral("Single"), QStringLiteral("Occurrence"), QStringLiteral("Exception"), QStringLiteral("RecurringMaster") }; static const QVector legacyFreeBusyStatusNames = { QStringLiteral("Free"), QStringLiteral("Tentative"), QStringLiteral("Busy"), QStringLiteral("OOF"), QStringLiteral("NoData") }; static const QVector responseTypeNames = { QStringLiteral("Unknown"), QStringLiteral("Organizer"), QStringLiteral("Tentative"), QStringLiteral("Accept"), QStringLiteral("Decline"), QStringLiteral("NoResponseReceived") }; bool ewsXmlBoolReader(QXmlStreamReader &reader, QVariant &val) { const QString elmText = reader.readElementText(); if (reader.error() != QXmlStreamReader::NoError) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading %1 element") .arg(reader.name().toString()); reader.skipCurrentElement(); return false; } if (elmText == QLatin1String("true")) { val = true; } else if (elmText == QLatin1String("false")) { val = false; } else { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected invalid boolean value in %1 element:") .arg(reader.name().toString()) << elmText; return false; } return true; } bool ewsXmlBoolWriter(QXmlStreamWriter &writer, const QVariant &val) { writer.writeCharacters(val.toBool() ? QStringLiteral("true") : QStringLiteral("false")); return true; } bool ewsXmlBase64Reader(QXmlStreamReader &reader, QVariant &val) { QString elmName = reader.name().toString(); val = QByteArray::fromBase64(reader.readElementText().toLatin1()); if (reader.error() != QXmlStreamReader::NoError) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid content.") .arg(elmName); reader.skipCurrentElement(); return false; } return true; } bool ewsXmlBase64Writer(QXmlStreamWriter &writer, const QVariant &val) { writer.writeCharacters(QString::fromLatin1(val.toByteArray().toBase64())); return true; } bool ewsXmlIdReader(QXmlStreamReader &reader, QVariant &val) { QString elmName = reader.name().toString(); EwsId id = EwsId(reader); if (id.type() == EwsId::Unspecified) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid content.") .arg(elmName); reader.skipCurrentElement(); return false; } val = QVariant::fromValue(id); reader.skipCurrentElement(); return true; } bool ewsXmlIdWriter(QXmlStreamWriter &writer, const QVariant &val) { EwsId id = val.value(); if (id.type() == EwsId::Unspecified) { return false; } id.writeAttributes(writer); return true; } bool ewsXmlTextReader(QXmlStreamReader &reader, QVariant &val) { QString elmName = reader.name().toString(); val = reader.readElementText(); if (reader.error() != QXmlStreamReader::NoError) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid content.") .arg(elmName); reader.skipCurrentElement(); return false; } return true; } bool ewsXmlTextWriter(QXmlStreamWriter &writer, const QVariant &val) { writer.writeCharacters(val.toString()); return true; } bool ewsXmlUIntReader(QXmlStreamReader &reader, QVariant &val) { QString elmName = reader.name().toString(); bool ok; val = reader.readElementText().toUInt(&ok); if (reader.error() != QXmlStreamReader::NoError || !ok) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid content.") .arg(elmName); return false; } return true; } bool ewsXmlUIntWriter(QXmlStreamWriter &writer, const QVariant &val) { writer.writeCharacters(QString::number(val.toUInt())); return true; } bool ewsXmlDateTimeReader(QXmlStreamReader &reader, QVariant &val) { QString elmName = reader.name().toString(); QDateTime dt = QDateTime::fromString(reader.readElementText(), Qt::ISODate); if (reader.error() != QXmlStreamReader::NoError || !dt.isValid()) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid content.") .arg(elmName); return false; } val = QVariant::fromValue(dt); return true; } bool ewsXmlItemReader(QXmlStreamReader &reader, QVariant &val) { QString elmName = reader.name().toString(); EwsItem item = EwsItem(reader); if (!item.isValid()) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid content.") .arg(elmName); reader.skipCurrentElement(); return false; } val = QVariant::fromValue(item); return true; } bool ewsXmlFolderReader(QXmlStreamReader &reader, QVariant &val) { const QString elmName = reader.name().toString(); EwsFolder folder = EwsFolder(reader); if (!folder.isValid()) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid content.") .arg(elmName); reader.skipCurrentElement(); return false; } val = QVariant::fromValue(folder); return true; } bool ewsXmlEnumReader(QXmlStreamReader &reader, QVariant &val, const QVector &items) { const QString elmName = reader.name().toString(); QString text = reader.readElementText(); if (reader.error() != QXmlStreamReader::NoError) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid content.") .arg(elmName); reader.skipCurrentElement(); return false; } int i = 0; QVector::const_iterator it; for (it = items.cbegin(); it != items.cend(); ++it, i++) { if (text == *it) { val = i; break; } } if (it == items.cend()) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - unknown value %2.") - .arg(elmName).arg(text); + .arg(elmName, text); return false; } return true; } bool ewsXmlSensitivityReader(QXmlStreamReader &reader, QVariant &val) { return ewsXmlEnumReader(reader, val, messageSensitivityNames); } bool ewsXmlImportanceReader(QXmlStreamReader &reader, QVariant &val) { return ewsXmlEnumReader(reader, val, messageImportanceNames); } bool ewsXmlCalendarItemTypeReader(QXmlStreamReader &reader, QVariant &val) { return ewsXmlEnumReader(reader, val, calendarItemTypeNames); } bool ewsXmlLegacyFreeBusyStatusReader(QXmlStreamReader &reader, QVariant &val) { return ewsXmlEnumReader(reader, val, legacyFreeBusyStatusNames); } bool ewsXmlResponseTypeReader(QXmlStreamReader &reader, QVariant &val) { return ewsXmlEnumReader(reader, val, responseTypeNames); } template<> QString readXmlElementValue(QXmlStreamReader &reader, bool &ok, const QString &parentElement) { ok = true; QStringRef elmName = reader.name(); QString val = reader.readElementText(); if (reader.error() != QXmlStreamReader::NoError) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid %2 element.") - .arg(parentElement).arg(elmName.toString()); + .arg(parentElement, elmName.toString()); reader.skipCurrentElement(); val.clear(); ok = false; } return val; } template<> int readXmlElementValue(QXmlStreamReader &reader, bool &ok, const QString &parentElement) { QStringRef elmName = reader.name(); QString valStr = readXmlElementValue(reader, ok, parentElement); int val = 0; if (ok) { val = valStr.toInt(&ok); if (!ok) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid %2 element.") - .arg(parentElement).arg(elmName.toString()); + .arg(parentElement, elmName.toString()); } } return val; } template<> long readXmlElementValue(QXmlStreamReader &reader, bool &ok, const QString &parentElement) { QStringRef elmName = reader.name(); QString valStr = readXmlElementValue(reader, ok, parentElement); long val = 0; if (ok) { val = valStr.toLong(&ok); if (!ok) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid %2 element.") - .arg(parentElement).arg(elmName.toString()); + .arg(parentElement, elmName.toString()); } } return val; } template<> QDateTime readXmlElementValue(QXmlStreamReader &reader, bool &ok, const QString &parentElement) { QStringRef elmName = reader.name(); QString valStr = readXmlElementValue(reader, ok, parentElement); QDateTime val; if (ok) { val = QDateTime::fromString(valStr, Qt::ISODate); if (!val.isValid()) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid %2 element.") - .arg(parentElement).arg(elmName.toString()); + .arg(parentElement, elmName.toString()); ok = false; } } return val; } template<> bool readXmlElementValue(QXmlStreamReader &reader, bool &ok, const QString &parentElement) { QStringRef elmName = reader.name(); QString valStr = readXmlElementValue(reader, ok, parentElement); bool val = false; if (ok) { if (valStr == QLatin1String("true")) { val = true; } else if (valStr == QLatin1String("false")) { val = false; } else { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid %2 element.") - .arg(parentElement).arg(elmName.toString()); + .arg(parentElement, elmName.toString()); ok = false; } } return val; } template<> QByteArray readXmlElementValue(QXmlStreamReader &reader, bool &ok, const QString &parentElement) { QString valStr = readXmlElementValue(reader, ok, parentElement); QByteArray val; if (ok) { /* QByteArray::fromBase64() does not perform any input validity checks and skips invalid input characters */ val = QByteArray::fromBase64(valStr.toLatin1()); } return val; } diff --git a/resources/ews/ewsmodifyitemflagsjob.cpp b/resources/ews/ewsmodifyitemflagsjob.cpp index d5261d8a5..d869f882c 100644 --- a/resources/ews/ewsmodifyitemflagsjob.cpp +++ b/resources/ews/ewsmodifyitemflagsjob.cpp @@ -1,96 +1,96 @@ /* 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 "ewsmodifyitemflagsjob.h" #include "ewsmodifyitemjob.h" #include "ewsitemhandler.h" using namespace Akonadi; EwsModifyItemFlagsJob::EwsModifyItemFlagsJob(EwsClient &client, QObject *parent, const Item::List &items, const QSet &addedFlags, const QSet &removedFlags) : EwsJob(parent) , mItems(items) , mClient(client) , mAddedFlags(addedFlags) , mRemovedFlags(removedFlags) { } EwsModifyItemFlagsJob::~EwsModifyItemFlagsJob() { } void EwsModifyItemFlagsJob::itemModifyFinished(KJob *job) { if (job->error()) { setErrorText(job->errorString()); emitResult(); return; } EwsModifyItemJob *req = qobject_cast(job); if (!req) { setErrorText(QStringLiteral("Invalid EwsModifyItemJob job object")); emitResult(); return; } mResultItems += req->items(); removeSubjob(job); if (subjobs().isEmpty()) { Q_ASSERT(mResultItems.size() == mItems.size()); emitResult(); } } void EwsModifyItemFlagsJob::start() { Item::List items[EwsItemTypeUnknown]; Q_FOREACH (const Item &item, mItems) { EwsItemType type = EwsItemHandler::mimeToItemType(item.mimeType()); if (type == EwsItemTypeUnknown) { - setErrorText(QStringLiteral("Unknown item type %1 for item %2").arg(item.mimeType()).arg(item.remoteId())); + setErrorText(QStringLiteral("Unknown item type %1 for item %2").arg(item.mimeType(), item.remoteId())); emitResult(); return; } else { items[type].append(item); } } bool started = false; for (int type = 0; type < EwsItemTypeUnknown; type++) { if (!items[static_cast(type)].isEmpty()) { EwsItemHandler *handler = EwsItemHandler::itemHandler(static_cast(type)); EwsModifyItemJob *job = handler->modifyItemJob(mClient, items[type], QSet() << "FLAGS", this); connect(job, &EwsModifyItemJob::result, this, &EwsModifyItemFlagsJob::itemModifyFinished); addSubjob(job); job->start(); started = true; } } if (!started) { setErrorText(QStringLiteral("No items to process")); emitResult(); } } diff --git a/resources/ews/ewsresource.cpp b/resources/ews/ewsresource.cpp index 1ceb9431d..3e2be4d7b 100644 --- a/resources/ews/ewsresource.cpp +++ b/resources/ews/ewsresource.cpp @@ -1,1411 +1,1411 @@ /* Copyright (C) 2015-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 "ewsresource.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "ewsfetchitemsjob.h" #include "ewsfetchfoldersjob.h" #include "ewsfetchfoldersincrjob.h" #include "ewsgetitemrequest.h" #include "ewsupdateitemrequest.h" #include "ewsmodifyitemflagsjob.h" #include "ewsmoveitemrequest.h" #include "ewsdeleteitemrequest.h" #include "ewscreatefolderrequest.h" #include "ewsmovefolderrequest.h" #include "ewsupdatefolderrequest.h" #include "ewsdeletefolderrequest.h" #include "ewssubscriptionmanager.h" #include "ewsgetfolderrequest.h" #include "ewsitemhandler.h" #include "ewsmodifyitemjob.h" #include "ewscreateitemjob.h" #include "ewsconfigdialog.h" #include "ewssettings.h" #include "auth/ewsabstractauth.h" #ifdef HAVE_SEPARATE_MTA_RESOURCE #include "ewscreateitemrequest.h" #endif #include "tags/ewstagstore.h" #include "tags/ewsupdateitemstagsjob.h" #include "tags/ewsglobaltagswritejob.h" #include "tags/ewsglobaltagsreadjob.h" #include "ewsresource_debug.h" #include "ewsresourceadaptor.h" #include "ewssettingsadaptor.h" #include "ewswalletadaptor.h" using namespace Akonadi; struct SpecialFolders { EwsDistinguishedId did; SpecialMailCollections::Type type; QString iconName; }; static const QVector specialFolderList = { {EwsDIdInbox, SpecialMailCollections::Inbox, QStringLiteral("mail-folder-inbox")}, {EwsDIdOutbox, SpecialMailCollections::Outbox, QStringLiteral("mail-folder-outbox")}, {EwsDIdSentItems, SpecialMailCollections::SentMail, QStringLiteral("mail-folder-sent")}, {EwsDIdDeletedItems, SpecialMailCollections::Trash, QStringLiteral("user-trash")}, {EwsDIdDrafts, SpecialMailCollections::Drafts, QStringLiteral("document-properties")} }; const QString EwsResource::akonadiEwsPropsetUuid = QStringLiteral("9bf757ae-69b5-4d8a-bf1d-2dd0c0871a28"); const EwsPropertyField EwsResource::globalTagsProperty(EwsResource::akonadiEwsPropsetUuid, QStringLiteral("GlobalTags"), EwsPropTypeStringArray); const EwsPropertyField EwsResource::globalTagsVersionProperty(EwsResource::akonadiEwsPropsetUuid, QStringLiteral("GlobalTagsVersion"), EwsPropTypeInteger); const EwsPropertyField EwsResource::tagsProperty(EwsResource::akonadiEwsPropsetUuid, QStringLiteral("Tags"), EwsPropTypeStringArray); const EwsPropertyField EwsResource::flagsProperty(EwsResource::akonadiEwsPropsetUuid, QStringLiteral("Flags"), EwsPropTypeStringArray); static Q_CONSTEXPR int InitialReconnectTimeout = 60; static Q_CONSTEXPR int ReconnectTimeout = 300; EwsResource::EwsResource(const QString &id) : Akonadi::ResourceBase(id) , mAuthStage(0) , mTagsRetrieved(false) , mReconnectTimeout(InitialReconnectTimeout) , mSettings(new EwsSettings(winIdForDialogs())) { mEwsClient.setUserAgent(mSettings->userAgent()); mEwsClient.setEnableNTLMv2(mSettings->enableNTLMv2()); changeRecorder()->fetchCollection(true); changeRecorder()->collectionFetchScope().setAncestorRetrieval(CollectionFetchScope::Parent); changeRecorder()->itemFetchScope().fetchFullPayload(true); changeRecorder()->itemFetchScope().setAncestorRetrieval(ItemFetchScope::Parent); changeRecorder()->itemFetchScope().setFetchModificationTime(false); changeRecorder()->itemFetchScope().setFetchTags(true); mRootCollection.setParentCollection(Collection::root()); mRootCollection.setName(name()); mRootCollection.setContentMimeTypes(QStringList() << Collection::mimeType() << KMime::Message::mimeType()); mRootCollection.setRights(Collection::ReadOnly); setScheduleAttributeSyncBeforeItemSync(true); // Load the sync state QByteArray data = QByteArray::fromBase64(mSettings->syncState().toLatin1()); if (!data.isEmpty()) { data = qUncompress(data); if (!data.isEmpty()) { QDataStream stream(data); stream >> mSyncState; } } data = QByteArray::fromBase64(mSettings->folderSyncState().toLatin1()); if (!data.isEmpty()) { data = qUncompress(data); if (!data.isEmpty()) { mFolderSyncState = QString::fromLatin1(data); } } setHierarchicalRemoteIdentifiersEnabled(true); mTagStore = new EwsTagStore(this); QMetaObject::invokeMethod(this, &EwsResource::delayedInit, Qt::QueuedConnection); connect(this, &AgentBase::reloadConfiguration, this, &EwsResource::reloadConfig); } EwsResource::~EwsResource() { } void EwsResource::delayedInit() { new EwsResourceAdaptor(this); new EwsSettingsAdaptor(mSettings.data()); new EwsWalletAdaptor(mSettings.data()); QDBusConnection::sessionBus().registerObject(QStringLiteral("/Settings"), mSettings.data(), QDBusConnection::ExportAdaptors); } void EwsResource::resetUrl() { Q_EMIT status(Running, i18nc("@info:status", "Connecting to Exchange server")); EwsGetFolderRequest *req = new EwsGetFolderRequest(mEwsClient, this); const EwsId::List folders {EwsId(EwsDIdMsgFolderRoot), EwsId(EwsDIdInbox)}; req->setFolderIds(folders); EwsFolderShape shape(EwsShapeIdOnly); shape << EwsPropertyField(QStringLiteral("folder:DisplayName")); // Use the opportunity of reading the root folder to read the tag data. shape << globalTagsProperty << globalTagsVersionProperty; req->setFolderShape(shape); connect(req, &EwsRequest::result, this, &EwsResource::rootFolderFetchFinished); req->start(); } void EwsResource::rootFolderFetchFinished(KJob *job) { EwsGetFolderRequest *req = qobject_cast(job); if (!req) { Q_EMIT status(Idle, i18nc("@info:status", "Unable to connect to Exchange server")); setTemporaryOffline(reconnectTimeout()); qCWarning(EWSRES_LOG) << QStringLiteral("Invalid EwsGetFolderRequest job object"); return; } if (req->error()) { Q_EMIT status(Idle, i18nc("@info:status", "Unable to connect to Exchange server")); setTemporaryOffline(reconnectTimeout()); qWarning() << "ERROR" << req->errorString(); return; } if (req->responses().size() != 2) { Q_EMIT status(Idle, i18nc("@info:status", "Unable to connect to Exchange server")); setTemporaryOffline(reconnectTimeout()); qCWarning(EWSRES_LOG) << QStringLiteral("Invalid number of responses received"); return; } EwsFolder folder = req->responses()[1].folder(); EwsId id = folder[EwsFolderFieldFolderId].value(); if (id.type() == EwsId::Real) { /* Since KDE PIM is heavily based on IMAP philosophy it would only consider for filtering * folders with the remote identifier set to "INBOX". While this is true for IMAP/POP3, Exchange * uses Base64-encoded strings with data private to the server. In order for mail filtering to work * the EWS resource has pretended that the inbox folder's remote name is "INBOX". Since KDE Applications * 17.12 this workaround is no longer needed, however in order to clean-up after old Akonadi EWS * installations the code below sets the correct Exchange id for the Inbox folder. * * At some day in the future this part of code can be removed too. */ Collection c; c.setRemoteId(QStringLiteral("INBOX")); CollectionFetchJob *job = new CollectionFetchJob(c, CollectionFetchJob::Base, this); job->setFetchScope(changeRecorder()->collectionFetchScope()); job->fetchScope().setResource(identifier()); job->fetchScope().setListFilter(CollectionFetchScope::Sync); job->setProperty("inboxId", id.id()); connect(job, &CollectionFetchJob::result, this, &EwsResource::adjustInboxRemoteIdFetchFinished); int inboxIdx = mSettings->serverSubscriptionList().indexOf(QLatin1String("INBOX")); if (inboxIdx >= 0) { QStringList subList = mSettings->serverSubscriptionList(); subList[inboxIdx] = id.id(); mSettings->setServerSubscriptionList(subList); } } folder = req->responses().first().folder(); id = folder[EwsFolderFieldFolderId].value(); if (id.type() == EwsId::Real) { mRootCollection.setRemoteId(id.id()); mRootCollection.setRemoteRevision(id.changeKey()); qCDebug(EWSRES_LOG) << "Root folder is " << id; emitReadyStatus(); if (mSettings->serverSubscription()) { mSubManager.reset(new EwsSubscriptionManager(mEwsClient, id, mSettings.data(), this)); connect(mSubManager.data(), &EwsSubscriptionManager::foldersModified, this, &EwsResource::foldersModifiedEvent); connect(mSubManager.data(), &EwsSubscriptionManager::folderTreeModified, this, &EwsResource::folderTreeModifiedEvent); connect(mSubManager.data(), &EwsSubscriptionManager::fullSyncRequested, this, &EwsResource::fullSyncRequestedEvent); /* Use a queued connection here as the connectionError() method will actually destroy the subscription manager. If this * was done with a direct connection this would have ended up with destroying the caller object followed by a crash. */ connect(mSubManager.data(), &EwsSubscriptionManager::connectionError, this, &EwsResource::connectionError, Qt::QueuedConnection); mSubManager->start(); } synchronizeCollectionTree(); mTagStore->readTags(folder[globalTagsProperty].toStringList(), folder[globalTagsVersionProperty].toInt()); } } void EwsResource::adjustInboxRemoteIdFetchFinished(KJob *job) { if (!job->error()) { CollectionFetchJob *fetchJob = qobject_cast(job); Q_ASSERT(fetchJob); if (!fetchJob->collections().isEmpty()) { Collection c = fetchJob->collections()[0]; c.setRemoteId(fetchJob->property("inboxId").toString()); CollectionModifyJob *modifyJob = new CollectionModifyJob(c, this); modifyJob->start(); } } } void EwsResource::retrieveCollections() { if (mRootCollection.remoteId().isNull()) { cancelTask(i18nc("@info:status", "Root folder id not known.")); return; } Q_EMIT status(Running, i18nc("@info:status", "Retrieving collection tree")); if (!mFolderSyncState.isEmpty() && !mRootCollection.isValid()) { /* When doing an incremental sync the real Akonadi identifier of the root collection must * be known, because the retrieved list of changes needs to include all parent folders up * to the root. None of the child collections are required to be valid, but the root must * be, as it needs to be the anchor point. */ CollectionFetchJob *fetchJob = new CollectionFetchJob(mRootCollection, CollectionFetchJob::Base); connect(fetchJob, &CollectionFetchJob::result, this, &EwsResource::rootCollectionFetched); fetchJob->start(); } else { doRetrieveCollections(); } synchronizeTags(); } void EwsResource::rootCollectionFetched(KJob *job) { if (job->error()) { qCWarning(EWSRES_LOG) << "ERROR" << job->errorString(); } else { CollectionFetchJob *fetchJob = qobject_cast(job); if (fetchJob && !fetchJob->collections().isEmpty()) { mRootCollection = fetchJob->collections().first(); qCDebugNC(EWSRES_LOG) << QStringLiteral("Root collection fetched: ") << mRootCollection; } } /* If the fetch failed for whatever reason force a full sync, which doesn't require the root * collection to be valid. */ if (!mRootCollection.isValid()) { mFolderSyncState.clear(); } doRetrieveCollections(); } void EwsResource::doRetrieveCollections() { if (mFolderSyncState.isEmpty()) { EwsFetchFoldersJob *job = new EwsFetchFoldersJob(mEwsClient, mRootCollection, this); connect(job, &EwsFetchFoldersJob::result, this, &EwsResource::fetchFoldersJobFinished); job->start(); } else { EwsFetchFoldersIncrJob *job = new EwsFetchFoldersIncrJob(mEwsClient, mFolderSyncState, mRootCollection, this); connect(job, &EwsFetchFoldersIncrJob::result, this, &EwsResource::fetchFoldersIncrJobFinished); job->start(); } } void EwsResource::connectionError() { Q_EMIT status(Broken, i18nc("@info:status", "Unable to connect to Exchange server")); setTemporaryOffline(reconnectTimeout()); } void EwsResource::retrieveItems(const Collection &collection) { QString rid = collection.remoteId(); EwsFetchItemsJob *job = new EwsFetchItemsJob(collection, mEwsClient, mSyncState.value(rid), mItemsToCheck.value(rid), mTagStore, this); job->setQueuedUpdates(mQueuedUpdates.value(collection.remoteId())); mQueuedUpdates.remove(collection.remoteId()); connect(job, &EwsFetchItemsJob::result, this, &EwsResource::itemFetchJobFinished); connect(job, &EwsFetchItemsJob::status, this, [this](int s, const QString &message) { Q_EMIT status(s, message); }); connect(job, &EwsFetchItemsJob::percent, this, [this](int p) { Q_EMIT percent(p); }); job->start(); } bool EwsResource::retrieveItems(const Item::List &items, const QSet &parts) { qCDebugNC(EWSRES_AGENTIF_LOG) << "retrieveItems: start " << items << parts; EwsGetItemRequest *req = new EwsGetItemRequest(mEwsClient, this); EwsId::List ids; ids.reserve(items.count()); for (const Item &item : items) { ids << EwsId(item.remoteId(), item.remoteRevision()); } req->setItemIds(ids); EwsItemShape shape(EwsShapeIdOnly); shape << EwsPropertyField(QStringLiteral("item:MimeContent")); req->setItemShape(shape); req->setProperty("items", QVariant::fromValue(items)); connect(req, &EwsGetItemRequest::result, this, &EwsResource::getItemsRequestFinished); req->start(); return true; } void EwsResource::getItemsRequestFinished(KJob *job) { if (job->error()) { qWarning() << "ERROR" << job->errorString(); cancelTask(i18nc("@info:status", "Failed to process items retrieval request")); return; } EwsGetItemRequest *req = qobject_cast(job); if (!req) { qCWarning(EWSRES_LOG) << QStringLiteral("Invalid EwsGetItemRequest job object"); cancelTask(i18nc("@info:status", "Failed to retrieve items - internal error")); return; } const Item::List items = req->property("items").value(); QHash itemHash; itemHash.reserve(items.count()); for (const Item &item : items) { itemHash.insert(item.remoteId(), item); } const EwsGetItemRequest::Response &resp = req->responses()[0]; if (!resp.isSuccess()) { qCWarningNC(EWSRES_AGENTIF_LOG) << QStringLiteral("retrieveItems: Item fetch failed."); cancelTask(i18nc("@info:status", "Failed to retrieve items")); return; } if (items.size() != req->responses().size()) { qCWarningNC(EWSRES_AGENTIF_LOG) << QStringLiteral("retrieveItems: incorrect number of responses."); cancelTask(i18nc("@info:status", "Failed to retrieve items - incorrect number of responses")); return; } Q_FOREACH (const EwsGetItemRequest::Response &resp, req->responses()) { const EwsItem &ewsItem = resp.item(); EwsId id = ewsItem[EwsItemFieldItemId].value(); auto it = itemHash.find(id.id()); if (it == itemHash.end()) { qCWarningNC(EWSRES_AGENTIF_LOG) << QStringLiteral("retrieveItems: Akonadi item not found for item %1.").arg(id.id()); cancelTask(i18nc("@info:status", "Failed to retrieve items - Akonadi item not found for item %1", id.id())); return; } EwsItemType type = ewsItem.internalType(); if (type == EwsItemTypeUnknown) { qCWarningNC(EWSRES_AGENTIF_LOG) << QStringLiteral("retrieveItems: Unknown item type for item %1!").arg(id.id()); cancelTask(i18nc("@info:status", "Failed to retrieve items - Unknown item type for item %1", id.id())); return; } if (!EwsItemHandler::itemHandler(type)->setItemPayload(*it, ewsItem)) { qCWarningNC(EWSRES_AGENTIF_LOG) << "retrieveItems: Failed to fetch item payload"; cancelTask(i18nc("@info:status", "Failed to fetch item payload")); return; } } qCDebugNC(EWSRES_AGENTIF_LOG) << "retrieveItems: done"; itemsRetrieved(itemHash.values().toVector()); } void EwsResource::reloadConfig() { mSubManager.reset(nullptr); mEwsClient.setUrl(mSettings->baseUrl()); setUpAuth(); mEwsClient.setAuth(mAuth.data()); } void EwsResource::configure(WId windowId) { QPointer dlg = new EwsConfigDialog(this, mEwsClient, windowId, mSettings.data()); if (dlg->exec()) { reloadConfig(); Q_EMIT configurationDialogAccepted(); } else { Q_EMIT configurationDialogRejected(); } delete dlg; } void EwsResource::fetchFoldersJobFinished(KJob *job) { emitReadyStatus(); EwsFetchFoldersJob *req = qobject_cast(job); if (!req) { qCWarning(EWSRES_LOG) << QStringLiteral("Invalid EwsFetchFoldersJob job object"); cancelTask(i18nc("@info:status", "Failed to retrieve folders - internal error")); return; } if (req->error()) { qWarning() << "ERROR" << req->errorString(); cancelTask(i18nc("@info:status", "Failed to process folders retrieval request")); return; } mFolderSyncState = req->syncState(); saveState(); collectionsRetrieved(req->folders()); fetchSpecialFolders(); } void EwsResource::fetchFoldersIncrJobFinished(KJob *job) { emitReadyStatus(); EwsFetchFoldersIncrJob *req = qobject_cast(job); if (!req) { qCWarning(EWSRES_LOG) << QStringLiteral("Invalid EwsFetchFoldersIncrJob job object"); cancelTask(i18nc("@info:status", "Invalid incremental folders retrieval request job object")); return; } if (req->error()) { qCWarningNC(EWSRES_LOG) << QStringLiteral("ERROR") << req->errorString(); /* Retry with a full sync. */ qCWarningNC(EWSRES_LOG) << QStringLiteral("Retrying with a full sync."); mFolderSyncState.clear(); doRetrieveCollections(); return; } mFolderSyncState = req->syncState(); saveState(); collectionsRetrievedIncremental(req->changedFolders(), req->deletedFolders()); if (!req->changedFolders().isEmpty() || !req->deletedFolders().isEmpty()) { fetchSpecialFolders(); } } void EwsResource::itemFetchJobFinished(KJob *job) { EwsFetchItemsJob *fetchJob = qobject_cast(job); if (!fetchJob) { qCWarningNC(EWSRES_LOG) << QStringLiteral("Invalid EwsFetchItemsJobjob object"); cancelTask(i18nc("@info:status", "Failed to retrieve items - internal error")); return; } if (job->error()) { qCWarningNC(EWSRES_LOG) << QStringLiteral("Item fetch error:") << job->errorString(); if (mSyncState.contains(fetchJob->collection().remoteId())) { qCDebugNC(EWSRES_LOG) << QStringLiteral("Retrying with empty state."); // Retry with a clear sync state. mSyncState.remove(fetchJob->collection().remoteId()); retrieveItems(fetchJob->collection()); } else { qCDebugNC(EWSRES_LOG) << QStringLiteral("Clean sync failed."); // No more hope cancelTask(i18nc("@info:status", "Failed to retrieve items")); return; } } else { mSyncState[fetchJob->collection().remoteId()] = fetchJob->syncState(); itemsRetrievedIncremental(fetchJob->changedItems(), fetchJob->deletedItems()); } saveState(); mItemsToCheck.remove(fetchJob->collection().remoteId()); emitReadyStatus(); } void EwsResource::itemChanged(const Akonadi::Item &item, const QSet &partIdentifiers) { qCDebugNC(EWSRES_AGENTIF_LOG) << "itemChanged: start " << item << partIdentifiers; EwsItemType type = EwsItemHandler::mimeToItemType(item.mimeType()); if (isEwsMessageItemType(type)) { qCWarningNC(EWSRES_AGENTIF_LOG) << "itemChanged: Item type not supported for changing"; cancelTask(i18nc("@info:status", "Item type not supported for changing")); } else { EwsModifyItemJob *job = EwsItemHandler::itemHandler(type)->modifyItemJob(mEwsClient, Item::List() << item, partIdentifiers, this); connect(job, &KJob::result, this, &EwsResource::itemChangeRequestFinished); job->start(); } } void EwsResource::itemsFlagsChanged(const Akonadi::Item::List &items, const QSet &addedFlags, const QSet &removedFlags) { qCDebug(EWSRES_AGENTIF_LOG) << "itemsFlagsChanged: start" << items << addedFlags << removedFlags; EwsModifyItemFlagsJob *job = new EwsModifyItemFlagsJob(mEwsClient, this, items, addedFlags, removedFlags); connect(job, &EwsModifyItemFlagsJob::result, this, &EwsResource::itemModifyFlagsRequestFinished); job->start(); } void EwsResource::itemModifyFlagsRequestFinished(KJob *job) { if (job->error()) { qCWarning(EWSRES_AGENTIF_LOG) << "itemsFlagsChanged:" << job->errorString(); cancelTask(i18nc("@info:status", "Failed to process item flags update request")); return; } EwsModifyItemFlagsJob *req = qobject_cast(job); if (!req) { qCWarning(EWSRES_AGENTIF_LOG) << "itemsFlagsChanged: Invalid EwsModifyItemFlagsJob job object"; cancelTask(i18nc("@info:status", "Failed to update item flags - internal error")); return; } qCDebug(EWSRES_AGENTIF_LOG) << "itemsFlagsChanged: done"; changesCommitted(req->items()); } void EwsResource::itemChangeRequestFinished(KJob *job) { if (job->error()) { qCWarningNC(EWSRES_AGENTIF_LOG) << "itemChanged: " << job->errorString(); cancelTask(i18nc("@info:status", "Failed to process item update request")); return; } EwsModifyItemJob *req = qobject_cast(job); if (!req) { qCWarningNC(EWSRES_AGENTIF_LOG) << "itemChanged: Invalid EwsModifyItemJob job object"; cancelTask(i18nc("@info:status", "Failed to update item - internal error")); return; } qCDebugNC(EWSRES_AGENTIF_LOG) << "itemChanged: done"; changesCommitted(req->items()); } void EwsResource::itemsMoved(const Item::List &items, const Collection &sourceCollection, const Collection &destinationCollection) { qCDebug(EWSRES_AGENTIF_LOG) << "itemsMoved: start" << items << sourceCollection << destinationCollection; EwsId::List ids; ids.reserve(items.count()); for (const Item &item : items) { EwsId id(item.remoteId(), item.remoteRevision()); ids.append(id); } EwsMoveItemRequest *req = new EwsMoveItemRequest(mEwsClient, this); req->setItemIds(ids); EwsId destId(destinationCollection.remoteId(), QString()); req->setDestinationFolderId(destId); req->setProperty("items", QVariant::fromValue(items)); req->setProperty("sourceCollection", QVariant::fromValue(sourceCollection)); req->setProperty("destinationCollection", QVariant::fromValue(destinationCollection)); connect(req, &KJob::result, this, &EwsResource::itemMoveRequestFinished); req->start(); } void EwsResource::itemMoveRequestFinished(KJob *job) { if (job->error()) { qCWarningNC(EWSRES_AGENTIF_LOG) << "itemsMoved: " << job->errorString(); cancelTask(i18nc("@info:status", "Failed to process item move request")); return; } EwsMoveItemRequest *req = qobject_cast(job); if (!req) { qCWarningNC(EWSRES_AGENTIF_LOG) << "itemsMoved: Invalid EwsMoveItemRequest job object"; cancelTask(i18nc("@info:status", "Failed to move item - internal error")); return; } Item::List items = job->property("items").value(); if (items.count() != req->responses().count()) { qCWarningNC(EWSRES_AGENTIF_LOG) << "itemsMoved: Invalid number of responses received from server"; cancelTask(i18nc("@info:status", "Failed to move item - invalid number of responses received from server")); return; } /* When moving a batch of items it is possible that the operation will fail for some of them. * Unfortunately Akonadi doesn't provide a way to report such partial success/failure. In order * to work around this in case of partial failure the source and destination folders will be * resynchronised. In order to avoid doing a full sync a hint will be provided in order to * indicate the item(s) to check. */ Item::List movedItems; EwsId::List failedIds; Collection srcCol = req->property("sourceCollection").value(); Collection dstCol = req->property("destinationCollection").value(); Item::List::iterator it = items.begin(); Q_FOREACH (const EwsMoveItemRequest::Response &resp, req->responses()) { Item &item = *it; if (resp.isSuccess()) { qCDebugNC(EWSRES_AGENTIF_LOG) << QStringLiteral("itemsMoved: succeeded for item %1 (new id: %2)") - .arg(ewsHash(item.remoteId())).arg(ewsHash(resp.itemId().id())); + .arg(ewsHash(item.remoteId()), ewsHash(resp.itemId().id())); if (item.isValid()) { /* Log item deletion in the source folder so that the next sync doesn't trip over * non-existent items. Use old remote ids for that. */ if (mSubManager) { mSubManager->queueUpdate(EwsDeletedEvent, item.remoteId(), QString()); } mQueuedUpdates[srcCol.remoteId()].append({item.remoteId(), QString(), EwsDeletedEvent}); item.setRemoteId(resp.itemId().id()); item.setRemoteRevision(resp.itemId().changeKey()); movedItems.append(item); } } else { Q_EMIT warning(QStringLiteral("Move failed for item %1").arg(item.remoteId())); qCDebugNC(EWSRES_AGENTIF_LOG) << QStringLiteral("itemsMoved: failed for item %1").arg(ewsHash(item.remoteId())); failedIds.append(EwsId(item.remoteId(), QString())); } ++it; } if (!failedIds.isEmpty()) { qCWarningNC(EWSRES_LOG) << QStringLiteral("Failed to move %1 items. Forcing src & dst folder sync.") .arg(failedIds.size()); mItemsToCheck[srcCol.remoteId()] += failedIds; foldersModifiedEvent(EwsId::List({EwsId(srcCol.remoteId(), QString())})); mItemsToCheck[dstCol.remoteId()] += failedIds; foldersModifiedEvent(EwsId::List({EwsId(dstCol.remoteId(), QString())})); } qCDebugNC(EWSRES_AGENTIF_LOG) << "itemsMoved: done"; changesCommitted(movedItems); } void EwsResource::itemsRemoved(const Item::List &items) { qCDebugNC(EWSRES_AGENTIF_LOG) << "itemsRemoved: start" << items; EwsId::List ids; ids.reserve(items.count()); for (const Item &item : items) { EwsId id(item.remoteId(), item.remoteRevision()); ids.append(id); } EwsDeleteItemRequest *req = new EwsDeleteItemRequest(mEwsClient, this); req->setItemIds(ids); req->setProperty("items", QVariant::fromValue(items)); connect(req, &EwsDeleteItemRequest::result, this, &EwsResource::itemDeleteRequestFinished); req->start(); } void EwsResource::itemDeleteRequestFinished(KJob *job) { if (job->error()) { qCWarningNC(EWSRES_AGENTIF_LOG) << "itemsRemoved: " << job->errorString(); cancelTask(i18nc("@info:status", "Failed to process item delete request")); return; } EwsDeleteItemRequest *req = qobject_cast(job); if (!req) { qCWarningNC(EWSRES_AGENTIF_LOG) << "itemsRemoved: Invalid EwsDeleteItemRequest job object"; cancelTask(i18nc("@info:status", "Failed to delete item - internal error")); return; } Item::List items = job->property("items").value(); if (items.count() != req->responses().count()) { qCWarningNC(EWSRES_AGENTIF_LOG) << "itemsRemoved: Invalid number of responses received from server"; cancelTask(i18nc("@info:status", "Failed to delete item - invalid number of responses received from server")); return; } /* When removing a batch of items it is possible that the operation will fail for some of them. * Unfortunately Akonadi doesn't provide a way to report such partial success/failure. In order * to work around this in case of partial failure the original folder(s) will be resynchronised. * In order to avoid doing a full sync a hint will be provided in order to indicate the item(s) * to check. */ EwsId::List foldersToSync; Item::List::iterator it = items.begin(); Q_FOREACH (const EwsDeleteItemRequest::Response &resp, req->responses()) { Item &item = *it; if (resp.isSuccess()) { qCDebugNC(EWSRES_AGENTIF_LOG) << QStringLiteral("itemsRemoved: succeeded for item %1").arg(ewsHash(item.remoteId())); if (mSubManager) { mSubManager->queueUpdate(EwsDeletedEvent, item.remoteId(), QString()); } mQueuedUpdates[item.parentCollection().remoteId()].append({item.remoteId(), QString(), EwsDeletedEvent}); } else { Q_EMIT warning(QStringLiteral("Delete failed for item %1").arg(item.remoteId())); qCWarningNC(EWSRES_AGENTIF_LOG) << QStringLiteral("itemsRemoved: failed for item %1").arg(ewsHash(item.remoteId())); EwsId colId = EwsId(item.parentCollection().remoteId(), QString()); mItemsToCheck[colId.id()].append(EwsId(item.remoteId(), QString())); if (!foldersToSync.contains(colId)) { foldersToSync.append(colId); } } ++it; } if (!foldersToSync.isEmpty()) { qCWarningNC(EWSRES_LOG) << QStringLiteral("Need to force sync for %1 folders.") .arg(foldersToSync.size()); foldersModifiedEvent(foldersToSync); } qCDebug(EWSRES_AGENTIF_LOG) << "itemsRemoved: done"; changeProcessed(); } void EwsResource::itemAdded(const Item &item, const Collection &collection) { EwsItemType type = EwsItemHandler::mimeToItemType(item.mimeType()); if (isEwsMessageItemType(type)) { cancelTask(i18nc("@info:status", "Item type not supported for creation")); } else { EwsCreateItemJob *job = EwsItemHandler::itemHandler(type)->createItemJob(mEwsClient, item, collection, mTagStore, this); connect(job, &EwsCreateItemJob::result, this, &EwsResource::itemCreateRequestFinished); job->start(); } } void EwsResource::itemCreateRequestFinished(KJob *job) { if (job->error()) { cancelTask(i18nc("@info:status", "Failed to process item create request")); return; } EwsCreateItemJob *req = qobject_cast(job); if (!req) { cancelTask(i18nc("@info:status", "Failed to create item - internal error")); return; } changeCommitted(req->item()); } void EwsResource::collectionAdded(const Collection &collection, const Collection &parent) { EwsFolderType type; QStringList mimeTypes = collection.contentMimeTypes(); if (mimeTypes.contains(EwsItemHandler::itemHandler(EwsItemTypeCalendarItem)->mimeType())) { type = EwsFolderTypeCalendar; } else if (mimeTypes.contains(EwsItemHandler::itemHandler(EwsItemTypeContact)->mimeType())) { type = EwsFolderTypeContacts; } else if (mimeTypes.contains(EwsItemHandler::itemHandler(EwsItemTypeTask)->mimeType())) { type = EwsFolderTypeTasks; } else if (mimeTypes.contains(EwsItemHandler::itemHandler(EwsItemTypeMessage)->mimeType())) { type = EwsFolderTypeMail; } else { qCWarningNC(EWSRES_LOG) << QStringLiteral("Cannot determine EWS folder type."); cancelTask(i18nc("@info:status", "Failed to add collection - cannot determine EWS folder type")); return; } EwsFolder folder; folder.setType(type); folder.setField(EwsFolderFieldDisplayName, collection.name()); EwsCreateFolderRequest *req = new EwsCreateFolderRequest(mEwsClient, this); req->setParentFolderId(EwsId(parent.remoteId())); req->setFolders(EwsFolder::List() << folder); req->setProperty("collection", QVariant::fromValue(collection)); connect(req, &EwsCreateFolderRequest::result, this, &EwsResource::folderCreateRequestFinished); req->start(); } void EwsResource::folderCreateRequestFinished(KJob *job) { if (job->error()) { cancelTask(i18nc("@info:status", "Failed to process folder create request")); return; } EwsCreateFolderRequest *req = qobject_cast(job); if (!req) { cancelTask(i18nc("@info:status", "Failed to create folder - internal error")); return; } Collection col = job->property("collection").value(); EwsCreateFolderRequest::Response resp = req->responses().first(); if (resp.isSuccess()) { const EwsId &id = resp.folderId(); col.setRemoteId(id.id()); col.setRemoteRevision(id.changeKey()); changeCommitted(col); } else { cancelTask(i18nc("@info:status", "Failed to create folder")); } } void EwsResource::collectionMoved(const Collection &collection, const Collection &collectionSource, const Collection &collectionDestination) { Q_UNUSED(collectionSource) EwsId::List ids; ids.append(EwsId(collection.remoteId(), collection.remoteRevision())); EwsMoveFolderRequest *req = new EwsMoveFolderRequest(mEwsClient, this); req->setFolderIds(ids); EwsId destId(collectionDestination.remoteId()); req->setDestinationFolderId(destId); req->setProperty("collection", QVariant::fromValue(collection)); connect(req, &EwsMoveFolderRequest::result, this, &EwsResource::folderMoveRequestFinished); req->start(); } void EwsResource::folderMoveRequestFinished(KJob *job) { if (job->error()) { cancelTask(i18nc("@info:status", "Failed to process folder move request")); return; } EwsMoveFolderRequest *req = qobject_cast(job); if (!req) { cancelTask(i18nc("@info:status", "Failed to move folder - internal error")); return; } Collection col = job->property("collection").value(); if (req->responses().count() != 1) { cancelTask(i18nc("@info:status", "Failed to move folder - invalid number of responses received from server")); return; } EwsMoveFolderRequest::Response resp = req->responses().first(); if (resp.isSuccess()) { const EwsId &id = resp.folderId(); col.setRemoteId(id.id()); col.setRemoteRevision(id.changeKey()); changeCommitted(col); } else { cancelTask(i18nc("@info:status", "Failed to move folder")); } } void EwsResource::collectionChanged(const Collection &collection, const QSet &changedAttributes) { if (changedAttributes.contains("NAME")) { EwsUpdateFolderRequest *req = new EwsUpdateFolderRequest(mEwsClient, this); EwsUpdateFolderRequest::FolderChange fc(EwsId(collection.remoteId(), collection.remoteRevision()), EwsFolderTypeMail); EwsUpdateFolderRequest::Update *upd = new EwsUpdateFolderRequest::SetUpdate(EwsPropertyField(QStringLiteral("folder:DisplayName")), collection.name()); fc.addUpdate(upd); req->addFolderChange(fc); req->setProperty("collection", QVariant::fromValue(collection)); connect(req, &EwsUpdateFolderRequest::finished, this, &EwsResource::folderUpdateRequestFinished); req->start(); } else { changeCommitted(collection); } } void EwsResource::collectionChanged(const Akonadi::Collection &collection) { Q_UNUSED(collection) } void EwsResource::folderUpdateRequestFinished(KJob *job) { if (job->error()) { cancelTask(i18nc("@info:status", "Failed to process folder update request")); return; } EwsUpdateFolderRequest *req = qobject_cast(job); if (!req) { cancelTask(i18nc("@info:status", "Failed to update folder - internal error")); return; } Collection col = job->property("collection").value(); if (req->responses().count() != 1) { cancelTask(i18nc("@info:status", "Failed to update folder - invalid number of responses received from server")); return; } EwsUpdateFolderRequest::Response resp = req->responses().first(); if (resp.isSuccess()) { const EwsId &id = resp.folderId(); col.setRemoteId(id.id()); col.setRemoteRevision(id.changeKey()); changeCommitted(col); } else { cancelTask(i18nc("@info:status", "Failed to update folder")); } } void EwsResource::collectionRemoved(const Collection &collection) { EwsDeleteFolderRequest *req = new EwsDeleteFolderRequest(mEwsClient, this); EwsId::List ids; ids.append(EwsId(collection.remoteId(), collection.remoteRevision())); req->setFolderIds(ids); connect(req, &EwsDeleteFolderRequest::result, this, &EwsResource::folderDeleteRequestFinished); req->start(); } void EwsResource::folderDeleteRequestFinished(KJob *job) { if (job->error()) { cancelTask(i18nc("@info:status", "Failed to process folder delete request")); return; } EwsDeleteFolderRequest *req = qobject_cast(job); if (!req) { cancelTask(i18nc("@info:status", "Failed to delete folder - internal error")); return; } EwsDeleteFolderRequest::Response resp = req->responses().first(); if (resp.isSuccess()) { changeProcessed(); } else { cancelTask(i18nc("@info:status", "Failed to delete folder")); mFolderSyncState.clear(); synchronizeCollectionTree(); } } void EwsResource::sendItem(const Akonadi::Item &item) { EwsItemType type = EwsItemHandler::mimeToItemType(item.mimeType()); if (isEwsMessageItemType(type)) { itemSent(item, TransportFailed, i18nc("@info:status", "Item type not supported for creation")); } else { EwsCreateItemJob *job = EwsItemHandler::itemHandler(type)->createItemJob(mEwsClient, item, Collection(), mTagStore, this); job->setSend(true); job->setProperty("item", QVariant::fromValue(item)); connect(job, &EwsCreateItemJob::result, this, &EwsResource::itemSendRequestFinished); job->start(); } } void EwsResource::itemSendRequestFinished(KJob *job) { Item item = job->property("item").value(); if (job->error()) { itemSent(item, TransportFailed, i18nc("@info:status", "Failed to process item send request")); return; } EwsCreateItemJob *req = qobject_cast(job); if (!req) { itemSent(item, TransportFailed, i18nc("@info:status", "Failed to send item - internal error")); return; } itemSent(item, TransportSucceeded); } void EwsResource::sendMessage(const QString &id, const QByteArray &content) { #ifdef HAVE_SEPARATE_MTA_RESOURCE EwsCreateItemRequest *req = new EwsCreateItemRequest(mEwsClient, this); EwsItem item; item.setType(EwsItemTypeMessage); item.setField(EwsItemFieldMimeContent, content); req->setItems(EwsItem::List() << item); req->setMessageDisposition(EwsDispSendOnly); req->setProperty("requestId", id); connect(req, &EwsCreateItemRequest::finished, this, &EwsResource::messageSendRequestFinished); req->start(); #endif } #ifdef HAVE_SEPARATE_MTA_RESOURCE void EwsResource::messageSendRequestFinished(KJob *job) { QString id = job->property("requestId").toString(); if (job->error()) { Q_EMIT messageSent(id, i18nc("@info:status", "Failed to process item send request")); return; } EwsCreateItemRequest *req = qobject_cast(job); if (!req) { Q_EMIT messageSent(id, i18nc("@info:status", "Failed to send item - internal error")); return; } if (req->responses().count() != 1) { Q_EMIT messageSent(id, i18nc("@info:status", "Invalid number of responses received from server")); return; } EwsCreateItemRequest::Response resp = req->responses().first(); if (resp.isSuccess()) { Q_EMIT messageSent(id, QString()); } else { Q_EMIT messageSent(id, resp.responseMessage()); } } #endif void EwsResource::foldersModifiedEvent(const EwsId::List &folders) { for (const EwsId &id : folders) { Collection c; c.setRemoteId(id.id()); CollectionFetchJob *job = new CollectionFetchJob(c, CollectionFetchJob::Base); job->setFetchScope(changeRecorder()->collectionFetchScope()); job->fetchScope().setResource(identifier()); job->fetchScope().setListFilter(CollectionFetchScope::Sync); connect(job, &KJob::result, this, &EwsResource::foldersModifiedCollectionSyncFinished); } } void EwsResource::foldersModifiedCollectionSyncFinished(KJob *job) { if (job->error()) { qCDebug(EWSRES_LOG) << QStringLiteral("Failed to fetch collection tree for sync."); return; } CollectionFetchJob *fetchJob = qobject_cast(job); synchronizeCollection(fetchJob->collections()[0].id()); } void EwsResource::folderTreeModifiedEvent() { synchronizeCollectionTree(); } void EwsResource::fullSyncRequestedEvent() { synchronize(); } void EwsResource::clearFolderSyncState() { mSyncState.clear(); saveState(); } void EwsResource::clearFolderSyncState(const QString &folderId) { mSyncState.remove(folderId); saveState(); } void EwsResource::clearFolderTreeSyncState() { mFolderSyncState.clear(); saveState(); } void EwsResource::fetchSpecialFolders() { CollectionFetchJob *job = new CollectionFetchJob(mRootCollection, CollectionFetchJob::Recursive, this); connect(job, &CollectionFetchJob::collectionsReceived, this, &EwsResource::specialFoldersCollectionsRetrieved); connect(job, &CollectionFetchJob::result, this, [](KJob *job) { if (job->error()) { qCWarningNC(EWSRES_LOG) << "Special folders fetch failed:" << job->errorString(); } }); job->start(); } void EwsResource::specialFoldersCollectionsRetrieved(const Collection::List &folders) { EwsId::List queryItems; queryItems.reserve(specialFolderList.count()); for (const SpecialFolders &sf : qAsConst(specialFolderList)) { queryItems.append(EwsId(sf.did)); } if (!queryItems.isEmpty()) { EwsGetFolderRequest *req = new EwsGetFolderRequest(mEwsClient, this); req->setFolderShape(EwsFolderShape(EwsShapeIdOnly)); req->setFolderIds(queryItems); req->setProperty("collections", QVariant::fromValue(folders)); connect(req, &EwsGetFolderRequest::finished, this, &EwsResource::specialFoldersFetchFinished); req->start(); } } void EwsResource::specialFoldersFetchFinished(KJob *job) { if (job->error()) { qCWarningNC(EWSRES_LOG) << QStringLiteral("Special collection fetch failed:") << job->errorString(); return; } EwsGetFolderRequest *req = qobject_cast(job); if (!req) { qCWarningNC(EWSRES_LOG) << QStringLiteral("Special collection fetch failed:") << QStringLiteral("Invalid EwsGetFolderRequest job object"); return; } const Collection::List collections = req->property("collections").value(); if (req->responses().size() != specialFolderList.size()) { qCWarningNC(EWSRES_LOG) << QStringLiteral("Special collection fetch failed:") << QStringLiteral("Invalid number of responses received"); return; } QMap map; for (const Collection &col : collections) { map.insert(col.remoteId(), col); } auto it = specialFolderList.cbegin(); Q_FOREACH (const EwsGetFolderRequest::Response &resp, req->responses()) { if (resp.isSuccess()) { EwsId fid = resp.folder()[EwsFolderFieldFolderId].value(); QMap::iterator mapIt = map.find(fid.id()); if (mapIt != map.end()) { qCDebugNC(EWSRES_LOG) << QStringLiteral("Registering folder %1(%2) as special collection %3") .arg(ewsHash(mapIt->remoteId())).arg(mapIt->id()).arg(it->type); SpecialMailCollections::self()->registerCollection(it->type, *mapIt); if (!mapIt->hasAttribute()) { EntityDisplayAttribute *attr = mapIt->attribute(Collection::AddIfMissing); attr->setIconName(it->iconName); CollectionModifyJob *modJob = new CollectionModifyJob(*mapIt, this); modJob->start(); } } } it++; } } void EwsResource::saveState() { QByteArray str; QDataStream dataStream(&str, QIODevice::WriteOnly); dataStream << mSyncState; mSettings->setSyncState(QString::fromLatin1(qCompress(str, 9).toBase64())); mSettings->setFolderSyncState(QString::fromLatin1(qCompress(mFolderSyncState.toLatin1(), 9).toBase64())); mSettings->save(); } void EwsResource::doSetOnline(bool online) { if (online) { reloadConfig(); } else { mSubManager.reset(nullptr); } } int EwsResource::reconnectTimeout() { // Return InitialReconnectTimeout for the first time, then ReconnectTimeout. int timeout = mReconnectTimeout; mReconnectTimeout = ReconnectTimeout; return timeout; } void EwsResource::itemsTagsChanged(const Item::List &items, const QSet &addedTags, const QSet &removedTags) { Q_UNUSED(addedTags) Q_UNUSED(removedTags) EwsUpdateItemsTagsJob *job = new EwsUpdateItemsTagsJob(items, mTagStore, mEwsClient, this); connect(job, &EwsUpdateItemsTagsJob::result, this, &EwsResource::itemsTagChangeFinished); job->start(); } void EwsResource::itemsTagChangeFinished(KJob *job) { if (job->error()) { cancelTask(i18nc("@info:status", "Failed to process item tags update request")); return; } EwsUpdateItemsTagsJob *updJob = qobject_cast(job); if (!updJob) { cancelTask(i18nc("@info:status", "Failed to update item tags - internal error")); return; } changesCommitted(updJob->items()); } void EwsResource::tagAdded(const Tag &tag) { mTagStore->addTag(tag); EwsGlobalTagsWriteJob *job = new EwsGlobalTagsWriteJob(mTagStore, mEwsClient, mRootCollection, this); connect(job, &EwsGlobalTagsWriteJob::result, this, &EwsResource::globalTagChangeFinished); job->start(); } void EwsResource::tagChanged(const Tag &tag) { mTagStore->addTag(tag); EwsGlobalTagsWriteJob *job = new EwsGlobalTagsWriteJob(mTagStore, mEwsClient, mRootCollection, this); connect(job, &EwsGlobalTagsWriteJob::result, this, &EwsResource::globalTagChangeFinished); job->start(); } void EwsResource::tagRemoved(const Tag &tag) { mTagStore->removeTag(tag); EwsGlobalTagsWriteJob *job = new EwsGlobalTagsWriteJob(mTagStore, mEwsClient, mRootCollection, this); connect(job, &EwsGlobalTagsWriteJob::result, this, &EwsResource::globalTagChangeFinished); job->start(); } void EwsResource::globalTagChangeFinished(KJob *job) { if (job->error()) { cancelTask(i18nc("@info:status", "Failed to process global tag update request")); } else { changeProcessed(); } } void EwsResource::retrieveTags() { EwsGlobalTagsReadJob *job = new EwsGlobalTagsReadJob(mTagStore, mEwsClient, mRootCollection, this); connect(job, &EwsGlobalTagsReadJob::result, this, &EwsResource::globalTagsRetrievalFinished); job->start(); } void EwsResource::globalTagsRetrievalFinished(KJob *job) { if (job->error()) { cancelTask(i18nc("@info:status", "Failed to process global tags retrieval request")); } else { EwsGlobalTagsReadJob *readJob = qobject_cast(job); Q_ASSERT(readJob); tagsRetrieved(readJob->tags(), QHash()); } } void EwsResource::setUpAuth() { EwsAbstractAuth *auth = mSettings->loadAuth(this); /* Use queued connections here to avoid stack overflow when the reauthentication proceeds through all stages. */ connect(auth, &EwsAbstractAuth::authSucceeded, this, &EwsResource::authSucceeded, Qt::QueuedConnection); connect(auth, &EwsAbstractAuth::authFailed, this, &EwsResource::authFailed, Qt::QueuedConnection); connect(auth, &EwsAbstractAuth::requestAuthFailed, this, &EwsResource::requestAuthFailed, Qt::QueuedConnection); qCDebugNC(EWSRES_LOG) << QStringLiteral("Initializing authentication"); mAuth.reset(auth); auth->init(); } void EwsResource::authSucceeded() { mAuthStage = 0; resetUrl(); } void EwsResource::reauthNotificationDismissed(bool accepted) { if (mReauthNotification) { mReauthNotification.clear(); if (accepted) { mAuth->authenticate(true); } else { - Q_EMIT authFailed(QStringLiteral("Interactive authentication request denied")); + authFailed(QStringLiteral("Interactive authentication request denied")); } } } void EwsResource::authFailed(const QString &error) { qCWarningNC(EWSRES_LOG) << "Authentication failed: " << error; reauthenticate(); } void EwsResource::reauthenticate() { switch (mAuthStage) { case 0: if (mAuth->authenticate(false)) { break; } else { ++mAuthStage; } /* fall through */ case 1: { const auto reauthPrompt = mAuth->reauthPrompt(); if (!reauthPrompt.isNull()) { mReauthNotification = new KNotification(QStringLiteral("auth-expired"), KNotification::Persistent, this); mReauthNotification->setText(reauthPrompt.arg(name())); mReauthNotification->setActions(QStringList(i18nc("@action:button", "Authenticate"))); mReauthNotification->setComponentName(QStringLiteral("akonadi_ews_resource")); auto acceptedFn = std::bind(&EwsResource::reauthNotificationDismissed, this, true); auto rejectedFn = std::bind(&EwsResource::reauthNotificationDismissed, this, false); connect(mReauthNotification.data(), &KNotification::action1Activated, this, acceptedFn); connect(mReauthNotification.data(), &KNotification::closed, this, rejectedFn); connect(mReauthNotification.data(), &KNotification::ignored, this, rejectedFn); mReauthNotification->sendEvent(); break; } } /* fall through */ case 2: Q_EMIT status(Broken, i18nc("@info:status", "Authentication failed")); break; } ++mAuthStage; } void EwsResource::requestAuthFailed() { qCWarningNC(EWSRES_LOG) << "requestAuthFailed - going offline"; if (mAuthStage == 0) { QTimer::singleShot(0, [&]() { setTemporaryOffline(reconnectTimeout()); }); Q_EMIT status(Broken, i18nc("@info:status", "Authentication failed")); reauthenticate(); } } void EwsResource::emitReadyStatus() { Q_EMIT status(Idle, i18nc("@info:status Resource is ready", "Ready")); Q_EMIT percent(0); } AKONADI_RESOURCE_MAIN(EwsResource) diff --git a/resources/ews/test/fakeserver/fakeewsconnection.cpp b/resources/ews/test/fakeserver/fakeewsconnection.cpp index 8e025a756..c33756413 100644 --- a/resources/ews/test/fakeserver/fakeewsconnection.cpp +++ b/resources/ews/test/fakeserver/fakeewsconnection.cpp @@ -1,443 +1,443 @@ /* Copyright (C) 2015-2017 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 "fakeewsconnection.h" #include #include #include #include #include #include #include #include "fakeewsserver.h" #include "fakeewsserver_debug.h" static const QHash responseCodes = { {200, QStringLiteral("OK")}, {400, QStringLiteral("Bad Request")}, {401, QStringLiteral("Unauthorized")}, {403, QStringLiteral("Forbidden")}, {404, QStringLiteral("Not Found")}, {405, QStringLiteral("Method Not Allowed")}, {500, QStringLiteral("Internal Server Error")} }; static Q_CONSTEXPR int streamingEventsHeartbeatIntervalSeconds = 5; FakeEwsConnection::FakeEwsConnection(QTcpSocket *sock, FakeEwsServer *parent) : QObject(parent) , mSock(sock) , mContentLength(0) , mKeepAlive(false) , mState(Initial) , mAuthenticated(false) { qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Got new EWS connection."); connect(mSock.data(), &QTcpSocket::disconnected, this, &FakeEwsConnection::disconnected); connect(mSock.data(), &QTcpSocket::readyRead, this, &FakeEwsConnection::dataAvailable); connect(&mDataTimer, &QTimer::timeout, this, &FakeEwsConnection::dataTimeout); connect(&mStreamingRequestHeartbeat, &QTimer::timeout, this, &FakeEwsConnection::streamingRequestHeartbeat); connect(&mStreamingRequestTimeout, &QTimer::timeout, this, &FakeEwsConnection::streamingRequestTimeout); } FakeEwsConnection::~FakeEwsConnection() { qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Connection closed."); } void FakeEwsConnection::disconnected() { deleteLater(); } void FakeEwsConnection::dataAvailable() { if (mState == Initial) { QByteArray line = mSock->readLine(); QList tokens = line.split(' '); mKeepAlive = false; if (tokens.size() < 3) { sendError(QStringLiteral("Invalid request header")); return; } if (tokens.at(0) != "POST") { sendError(QStringLiteral("Expected POST request")); return; } if (tokens.at(1) != "/EWS/Exchange.asmx") { sendError(QStringLiteral("Invalid EWS URL")); return; } mState = RequestReceived; } if (mState == RequestReceived) { QByteArray line; do { line = mSock->readLine(); if (line.toLower().startsWith(QByteArray("content-length: "))) { bool ok; mContentLength = line.trimmed().mid(16).toUInt(&ok); if (!ok) { sendError(QStringLiteral("Failed to parse content length.")); return; } } else if (line.toLower().startsWith(QByteArray("authorization: basic "))) { if (line.trimmed().mid(21) == "dGVzdDp0ZXN0") { mAuthenticated = true; } } else if (line.toLower() == "connection: keep-alive\r\n") { mKeepAlive = true; } } while (!line.trimmed().isEmpty()); if (line == "\r\n") { mState = HeadersReceived; } } if (mState == HeadersReceived) { if (mContentLength == 0) { sendError(QStringLiteral("Expected content")); return; } mContent += mSock->read(mContentLength - mContent.size()); if (mContent.size() >= static_cast(mContentLength)) { mDataTimer.stop(); if (!mAuthenticated) { QString codeStr = responseCodes.value(401); QString response(QStringLiteral("HTTP/1.1 %1 %2\r\n" "WWW-Authenticate: Basic realm=\"Fake EWS Server\"\r\n" "Connection: close\r\n" "\r\n").arg(401).arg(codeStr)); response += codeStr; mSock->write(response.toLatin1()); mSock->disconnectFromHost(); return; } FakeEwsServer::DialogEntry::HttpResponse resp = parseRequest(QString::fromUtf8(mContent)); bool chunked = false; if (resp == FakeEwsServer::EmptyResponse) { resp = handleGetEventsRequest(QString::fromUtf8(mContent)); } if (resp == FakeEwsServer::EmptyResponse) { resp = handleGetStreamingEventsRequest(QString::fromUtf8(mContent)); if (resp.second > 1000) { chunked = true; resp.second %= 1000; } } FakeEwsServer *server = qobject_cast(parent()); auto defaultReplyCallback = server->defaultReplyCallback(); if (defaultReplyCallback && (resp == FakeEwsServer::EmptyResponse)) { QXmlResultItems ri; QXmlNamePool namePool; resp = defaultReplyCallback(QString::fromUtf8(mContent), ri, namePool); qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Returning response from default callback ") << resp.second << QStringLiteral(": ") << resp.first; } if (resp == FakeEwsServer::EmptyResponse) { qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Returning default response 500."); - resp = { QStringLiteral(""), 500 }; + resp = { QLatin1String(""), 500 }; } QByteArray buffer; QString codeStr = responseCodes.value(resp.second); QByteArray respContent = resp.first.toUtf8(); buffer += QStringLiteral("HTTP/1.1 %1 %2\r\n").arg(resp.second).arg(codeStr).toLatin1(); if (chunked) { buffer += "Transfer-Encoding: chunked\r\n"; buffer += "\r\n"; buffer += QByteArray::number(respContent.size(), 16) + "\r\n"; buffer += respContent + "\r\n"; } else { buffer += "Content-Length: " + QByteArray::number(respContent.size()) + "\r\n"; buffer += mKeepAlive ? "Connection: Keep-Alive\n" : "Connection: Close\r\n"; buffer += "\r\n"; buffer += respContent; } mSock->write(buffer); if (!mKeepAlive && !chunked) { mSock->disconnectFromHost(); } mContent.clear(); mState = Initial; } else { mDataTimer.start(3000); } } } void FakeEwsConnection::sendError(const QString &msg, ushort code) { qCWarningNC(EWSFAKE_LOG) << msg; QString codeStr = responseCodes.value(code); QByteArray response(QStringLiteral("HTTP/1.1 %1 %2\nConnection: close\n\n").arg(code).arg(codeStr).toLatin1()); response += msg.toLatin1(); mSock->write(response); mSock->disconnectFromHost(); } void FakeEwsConnection::dataTimeout() { qCWarning(EWSFAKE_LOG) << QLatin1String("Timeout waiting for content."); sendError(QStringLiteral("Timeout waiting for content.")); } FakeEwsServer::DialogEntry::HttpResponse FakeEwsConnection::parseRequest(const QString &content) { qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Got request: ") << content; FakeEwsServer *server = qobject_cast(parent()); FakeEwsServer::DialogEntry::HttpResponse resp = FakeEwsServer::EmptyResponse; Q_FOREACH (const FakeEwsServer::DialogEntry &de, server->dialog()) { QXmlResultItems ri; QByteArray resultBytes; QString result; QBuffer resultBuffer(&resultBytes); resultBuffer.open(QIODevice::WriteOnly); QXmlQuery query; QXmlSerializer xser(query, &resultBuffer); if (!de.xQuery.isNull()) { query.setFocus(content); query.setQuery(de.xQuery); query.evaluateTo(&xser); query.evaluateTo(&ri); if (ri.hasError()) { qCDebugNC(EWSFAKE_LOG) << QStringLiteral("XQuery failed due to errors - skipping"); continue; } result = QString::fromUtf8(resultBytes); } if (!result.trimmed().isEmpty()) { qCDebugNC(EWSFAKE_LOG) << QStringLiteral("Got match for \"") << de.description << QStringLiteral("\""); if (de.replyCallback) { resp = de.replyCallback(content, ri, query.namePool()); qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Returning response from callback ") << resp.second << QStringLiteral(": ") << resp.first; } else { resp = {result.trimmed(), 200}; qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Returning response from XQuery ") << resp.second << QStringLiteral(": ") << resp.first; } break; } } if (resp == FakeEwsServer::EmptyResponse) { qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Returning empty response."); qCInfoNC(EWSFAKE_LOG) << content; } return resp; } FakeEwsServer::DialogEntry::HttpResponse FakeEwsConnection::handleGetEventsRequest(const QString &content) { const QRegularExpression re(QStringLiteral( "].*<\\w*:?SubscriptionId>(?[^<]*)<\\w*:?Watermark>(?[^<]*).*")); QRegularExpressionMatch match = re.match(content); if (!match.hasMatch() || match.hasPartialMatch()) { qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Not a valid GetEvents request."); return FakeEwsServer::EmptyResponse; } qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Got valid GetEvents request."); QString resp = QStringLiteral("" "" "" "" "" "" "" "" "" "NoError" ""); if (match.captured(QStringLiteral("subid")).isEmpty() || match.captured(QStringLiteral("watermark")).isEmpty()) { qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Missing subscription id or watermark."); const QString errorResp = QStringLiteral("" "" "" "" "" "" "" "" "" "Missing subscription id or watermark." "ErrorInvalidPullSubscriptionId" "0" "" "" "" "" ""); return {errorResp, 200}; } resp += QLatin1String("") + match.captured(QLatin1String("subid")) + QLatin1String(""); resp += QLatin1String("") + match.captured(QLatin1String("watermark")) + QLatin1String(""); resp += QStringLiteral("false"); FakeEwsServer *server = qobject_cast(parent()); const QStringList events = server->retrieveEventsXml(); qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Returning %1 events.").arg(events.size()); for (const QString &eventXml : events) { resp += eventXml; } resp += QStringLiteral("" ""); return {resp, 200}; } QString FakeEwsConnection::prepareEventsResponse(const QStringList &events) { QString resp = QStringLiteral("" "" "" "" "" "" "" "" "" "NoError" "OK"); if (!events.isEmpty()) { resp += QLatin1String("") + mStreamingSubId + QLatin1String(""); qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Returning %1 events.").arg(events.size()); Q_FOREACH (const QString &eventXml, events) { resp += eventXml; } resp += QStringLiteral(""); } resp += QStringLiteral("" ""); return resp; } FakeEwsServer::DialogEntry::HttpResponse FakeEwsConnection::handleGetStreamingEventsRequest(const QString &content) { const QRegularExpression re(QStringLiteral( "].*<\\w*:?SubscriptionIds><\\w*:?SubscriptionId>(?[^<]*).*<\\w*:?ConnectionTimeout>(?[^<]*).*")); QRegularExpressionMatch match = re.match(content); if (!match.hasMatch() || match.hasPartialMatch()) { qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Not a valid GetStreamingEvents request."); return FakeEwsServer::EmptyResponse; } qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Got valid GetStreamingEvents request."); if (match.captured(QStringLiteral("subid")).isEmpty() || match.captured(QStringLiteral("timeout")).isEmpty()) { qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Missing subscription id or timeout."); const QString errorResp = QStringLiteral("" "" "" "" "" "" "" "" "" "Missing subscription id or timeout." "ErrorInvalidSubscription" "0" "" "" "" "" ""); return {errorResp, 200}; } mStreamingSubId = match.captured(QStringLiteral("subid")); FakeEwsServer *server = qobject_cast(parent()); const QStringList events = server->retrieveEventsXml(); QString resp = prepareEventsResponse(events); mStreamingRequestTimeout.start(match.captured(QStringLiteral("timeout")).toInt() * 1000 * 60); mStreamingRequestHeartbeat.setSingleShot(false); mStreamingRequestHeartbeat.start(streamingEventsHeartbeatIntervalSeconds * 1000); Q_EMIT streamingRequestStarted(this); return {resp, 1200}; } void FakeEwsConnection::streamingRequestHeartbeat() { sendEvents(QStringList()); } void FakeEwsConnection::streamingRequestTimeout() { mStreamingRequestTimeout.stop(); mStreamingRequestHeartbeat.stop(); mSock->write("0\r\n\r\n"); mSock->disconnectFromHost(); } void FakeEwsConnection::sendEvents(const QStringList &events) { QByteArray resp = prepareEventsResponse(events).toUtf8(); mSock->write(QByteArray::number(resp.size(), 16) + "\r\n" + resp + "\r\n"); } diff --git a/resources/ews/test/fakeserver/fakeewsserverthread.cpp b/resources/ews/test/fakeserver/fakeewsserverthread.cpp index 074503ec7..e46f2e8cd 100644 --- a/resources/ews/test/fakeserver/fakeewsserverthread.cpp +++ b/resources/ews/test/fakeserver/fakeewsserverthread.cpp @@ -1,111 +1,111 @@ /* Copyright (C) 2017 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 "fakeewsserverthread.h" #include #include "fakeewsserver.h" #include "fakeewsserver_debug.h" FakeEwsServerThread::FakeEwsServerThread(QObject *parent) : QThread(parent) , mPortNumber(0) , mIsRunning(0) { } FakeEwsServerThread::~FakeEwsServerThread() { } void FakeEwsServerThread::run() { qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Starting fake server thread"); mMutex.lock(); mServer.reset(new FakeEwsServer(nullptr)); bool ok = mServer->start(); mMutex.unlock(); if (ok) { qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Fake server thread started."); mPortNumber = mServer->portNumber(); Q_EMIT serverStarted(ok); mIsRunning = 1; exec(); qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Fake server thread terminating."); } else { Q_EMIT serverStarted(ok); qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Fake server thread start failed."); } mMutex.lock(); mServer.reset(); mMutex.unlock(); } void FakeEwsServerThread::setDialog(const FakeEwsServer::DialogEntry::List &dialog) { QMutexLocker lock(&mMutex); if (mServer) { mServer->setDialog(dialog); } } -void FakeEwsServerThread::setDefaultReplyCallback(FakeEwsServer::DialogEntry::ReplyCallback defaultReplyCallback) +void FakeEwsServerThread::setDefaultReplyCallback(const FakeEwsServer::DialogEntry::ReplyCallback &defaultReplyCallback) { QMutexLocker lock(&mMutex); if (mServer) { mServer->setDefaultReplyCallback(defaultReplyCallback); } } void FakeEwsServerThread::queueEventsXml(const QStringList &events) { QMutexLocker lock(&mMutex); if (mServer) { metaObject()->invokeMethod(this, "doQueueEventsXml", Q_ARG(QStringList, events)); } } void FakeEwsServerThread::doQueueEventsXml(const QStringList &events) { mServer->queueEventsXml(events); } bool FakeEwsServerThread::waitServerStarted() const { QEventLoop loop; { QMutexLocker lock(&mMutex); if (isFinished()) { return false; } if (mIsRunning) { return true; } connect(this, &FakeEwsServerThread::serverStarted, this, [&loop](bool ok) { loop.exit(ok ? 1 : 0); }); } return loop.exec(); } diff --git a/resources/ews/test/fakeserver/fakeewsserverthread.h b/resources/ews/test/fakeserver/fakeewsserverthread.h index 8cd4da140..1a9557cba 100644 --- a/resources/ews/test/fakeserver/fakeewsserverthread.h +++ b/resources/ews/test/fakeserver/fakeewsserverthread.h @@ -1,63 +1,63 @@ /* Copyright (C) 2017 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. */ #ifndef FAKEEWSSERVERTHREAD_H #define FAKEEWSSERVERTHREAD_H #include #include #include "fakeewsserver.h" class Q_DECL_EXPORT FakeEwsServerThread : public QThread { Q_OBJECT public: explicit FakeEwsServerThread(QObject *parent = nullptr); ~FakeEwsServerThread() override; // FakeEwsServer *server() const; ushort portNumber() const { return mPortNumber; } bool isRunning() const { return mIsRunning == 1; } void setDialog(const FakeEwsServer::DialogEntry::List &dialog); - void setDefaultReplyCallback(FakeEwsServer::DialogEntry::ReplyCallback defaultReplyCallback); + void setDefaultReplyCallback(const FakeEwsServer::DialogEntry::ReplyCallback &defaultReplyCallback); void queueEventsXml(const QStringList &events); bool waitServerStarted() const; Q_SIGNALS: void serverStarted(bool ok); protected: void run() override; private Q_SLOTS: void doQueueEventsXml(const QStringList &events); private: QScopedPointer mServer; ushort mPortNumber; QAtomicInt mIsRunning; mutable QMutex mMutex; }; #endif diff --git a/resources/ews/test/statemonitor.h b/resources/ews/test/statemonitor.h index aaa9f887b..6dfc17373 100644 --- a/resources/ews/test/statemonitor.h +++ b/resources/ews/test/statemonitor.h @@ -1,129 +1,130 @@ /* Copyright (C) 2017 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. */ #ifndef TEST_STATEMONITOR_H #define TEST_STATEMONITOR_H #include #include #include #include #include #include class StateMonitorBase : public QObject { Q_OBJECT public: explicit StateMonitorBase(QObject *parent) : QObject(parent) { } ~StateMonitorBase() override = default; Q_SIGNALS: void stateReached(); void errorOccurred(); }; template class CollectionStateMonitor : public StateMonitorBase { public: typedef std::function StateComparisonFunc; CollectionStateMonitor(QObject *parent, const QHash &stateHash, const QString &inboxId, const StateComparisonFunc &comparisonFunc, int recheckInterval = 0); ~CollectionStateMonitor() override = default; Akonadi::Monitor &monitor() { return mMonitor; } void forceRecheck(); private: void stateChanged(const Akonadi::Collection &col); Akonadi::Monitor mMonitor; QSet mPending; const QHash &mStateHash; StateComparisonFunc mComparisonFunc; const QString &mInboxId; QTimer mRecheckTimer; }; template CollectionStateMonitor::CollectionStateMonitor(QObject *parent, const QHash &stateHash, const QString &inboxId, const StateComparisonFunc &comparisonFunc, int recheckInterval) : StateMonitorBase(parent) , mMonitor(this) , mPending(stateHash.keys().toSet()) , mStateHash(stateHash) , mComparisonFunc(comparisonFunc) , mInboxId(inboxId) , mRecheckTimer(this) { connect(&mMonitor, &Akonadi::Monitor::collectionAdded, this, [this](const Akonadi::Collection &col, const Akonadi::Collection &) { stateChanged(col); }); connect(&mMonitor, QOverload::of(&Akonadi::Monitor::collectionChanged), this, [this](const Akonadi::Collection &col) { stateChanged(col); }); if (recheckInterval > 0) { mRecheckTimer.setInterval(recheckInterval); connect(&mRecheckTimer, &QTimer::timeout, this, &CollectionStateMonitor::forceRecheck); mRecheckTimer.start(); } } template void CollectionStateMonitor::stateChanged(const Akonadi::Collection &col) { auto remoteId = col.remoteId(); auto state = mStateHash.find(remoteId); if (state == mStateHash.end()) { qDebug() << "Cannot find state for collection" << remoteId; Q_EMIT errorOccurred(); } if (mComparisonFunc(col, *state)) { mPending.remove(remoteId); } else { mPending.insert(remoteId); } if (mPending.empty()) { Q_EMIT stateReached(); } } template void CollectionStateMonitor::forceRecheck() { auto fetchJob = new Akonadi::CollectionFetchJob(Akonadi::Collection::root(), Akonadi::CollectionFetchJob::Recursive, this); fetchJob->setFetchScope(mMonitor.collectionFetchScope()); if (fetchJob->exec()) { - for (const auto &col : fetchJob->collections()) { + const auto collections = fetchJob->collections(); + for (const auto &col : collections) { const auto remoteId = col.remoteId(); const auto state = mStateHash.find(remoteId); if (state != mStateHash.end()) { stateChanged(col); } } } } #endif diff --git a/resources/ews/test/unittests/ewsoauth_ut.cpp b/resources/ews/test/unittests/ewsoauth_ut.cpp index c265a8e00..ae6ce5930 100644 --- a/resources/ews/test/unittests/ewsoauth_ut.cpp +++ b/resources/ews/test/unittests/ewsoauth_ut.cpp @@ -1,346 +1,346 @@ /* 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 #include #include #include "auth/ewsoauth.h" #include "ewsoauth_ut_mock.h" static const QString testEmail = QStringLiteral("joe.bloggs@unknown.com"); static const QString testClientId = QStringLiteral("b43c59cd-dd1c-41fd-bb9a-b0a1d5696a93"); static const QString testReturnUri = QStringLiteral("urn:ietf:wg:oauth:2.0:oob"); //static const QString testReturnUriPercent = QUrl::toPercentEncoding(testReturnUri); static const QString testState = QStringLiteral("joidsiuhq"); static const QString resource = QStringLiteral("https://outlook.office365.com/"); //static const QString resourcePercent = QUrl::toPercentEncoding(resource); static const QString authUrl = QStringLiteral("https://login.microsoftonline.com/common/oauth2/authorize"); static const QString tokenUrl = QStringLiteral("https://login.microsoftonline.com/common/oauth2/token"); static const QString accessToken1 = QStringLiteral("IERbOTo5NSdtY5HMntWTH1wgrRt98KmbF7nNloIdZ4SSYOU7pziJJakpHy8r6kxQi+7T9w36mWv9IWLrvEwTsA"); static const QString refreshToken1 = QStringLiteral("YW7lJFWcEISynbraq4NiLLke3rOieFdvoJEDxpjCXorJblIGM56OJSu1PZXMCQL5W3KLxS9ydxqLHxRTSdw"); static const QString idToken1 = QStringLiteral("gz7l0chu9xIi1MMgPkpHGQTmo3W7L1rQbmWAxEL5VSKHeqdIJ7E3K7vmMYTl/C1fWihB5XiLjD2GSVQoOzTfCw"); class UtEwsOAuth : public QObject { Q_OBJECT private Q_SLOTS: void initialInteractiveSuccessful(); void initialRefreshSuccessful(); void refreshSuccessful(); private: static QString formatJsonSorted(const QVariantMap &map); static int performAuthAction(EwsOAuth &oAuth, int timeout, std::function actionFn); static void setUpAccessFunction(const QString &refreshToken); static void setUpTokenFunction(const QString &accessToken, const QString &refreshToken, const QString &idToken, quint64 time, int tokenLifetime, int extTokenLifetime, QString &tokenReplyData); static void dumpEvents(const QStringList &events, const QStringList &expectedEvents); - void setUpOAuth(EwsOAuth &oAuth, QStringList &events, QString password, QMap map); + void setUpOAuth(EwsOAuth &oAuth, QStringList &events, const QString &password, const QMap &map); }; void UtEwsOAuth::initialInteractiveSuccessful() { EwsOAuth oAuth(nullptr, testEmail, testClientId, testReturnUri); QVERIFY(Mock::QWebEngineView::instance); QVERIFY(Mock::QOAuth2AuthorizationCodeFlow::instance); QStringList events; setUpOAuth(oAuth, events, QString(), QMap()); Mock::QWebEngineView::instance->setRedirectUri(Mock::QOAuth2AuthorizationCodeFlow::instance->redirectUri()); auto time = QDateTime::currentSecsSinceEpoch(); constexpr unsigned int tokenLifetime = 86399; constexpr unsigned int extTokenLifetime = 345599; QString tokenReplyData; setUpAccessFunction(refreshToken1); setUpTokenFunction(accessToken1, refreshToken1, idToken1, time, tokenLifetime, extTokenLifetime, tokenReplyData); Mock::QOAuth2AuthorizationCodeFlow::instance->setState(testState); const auto initStatus = performAuthAction(oAuth, 1000, [](EwsOAuth *oAuth) { oAuth->init(); return true; }); QVERIFY(initStatus == 1); const auto authStatus = performAuthAction(oAuth, 2000, [](EwsOAuth *oAuth) { return oAuth->authenticate(true); }); QVERIFY(authStatus == 0); const auto authUrlString = Mock::authUrlString(authUrl, testClientId, testReturnUri, testEmail, resource, testState); const QStringList expectedEvents = { Mock::requestWalletMapString(), Mock::modifyParamsAuthString(testClientId, testReturnUri, testState), Mock::authorizeWithBrowserString(authUrlString), Mock::loadWebPageString(authUrlString), Mock::interceptRequestString(authUrlString), Mock::interceptRequestBlockedString(false), Mock::interceptRequestString(testReturnUri + QStringLiteral("?code=") + QString::fromLatin1(QUrl::toPercentEncoding(refreshToken1))), Mock::interceptRequestBlockedString(true), Mock::authorizationCallbackReceivedString(refreshToken1), Mock::modifyParamsTokenString(testClientId, testReturnUri, refreshToken1), Mock::networkReplyFinishedString(tokenReplyData), Mock::replyDataCallbackString(tokenReplyData), Mock::tokenCallbackString(accessToken1, refreshToken1, idToken1, time, tokenLifetime, extTokenLifetime, resource) }; dumpEvents(events, expectedEvents); QVERIFY(events == expectedEvents); } void UtEwsOAuth::initialRefreshSuccessful() { EwsOAuth oAuth(nullptr, testEmail, testClientId, testReturnUri); QVERIFY(Mock::QWebEngineView::instance); QVERIFY(Mock::QOAuth2AuthorizationCodeFlow::instance); QStringList events; QMap map = { {QStringLiteral("refresh-token"), refreshToken1} }; setUpOAuth(oAuth, events, QString(), map); Mock::QWebEngineView::instance->setRedirectUri(Mock::QOAuth2AuthorizationCodeFlow::instance->redirectUri()); auto time = QDateTime::currentSecsSinceEpoch(); constexpr unsigned int tokenLifetime = 86399; constexpr unsigned int extTokenLifetime = 345599; QString tokenReplyData; setUpAccessFunction(refreshToken1); setUpTokenFunction(accessToken1, refreshToken1, idToken1, time, tokenLifetime, extTokenLifetime, tokenReplyData); Mock::QOAuth2AuthorizationCodeFlow::instance->setState(testState); const auto initStatus = performAuthAction(oAuth, 1000, [](EwsOAuth *oAuth) { oAuth->init(); return true; }); QVERIFY(initStatus == 1); const auto authStatus = performAuthAction(oAuth, 2000, [](EwsOAuth *oAuth) { return oAuth->authenticate(true); }); QVERIFY(authStatus == 0); const auto authUrlString = Mock::authUrlString(authUrl, testClientId, testReturnUri, testEmail, resource, testState); const QStringList expectedEvents = { Mock::requestWalletMapString(), Mock::modifyParamsTokenString(testClientId, testReturnUri, refreshToken1), Mock::networkReplyFinishedString(tokenReplyData), Mock::replyDataCallbackString(tokenReplyData), Mock::tokenCallbackString(accessToken1, refreshToken1, idToken1, time, tokenLifetime, extTokenLifetime, resource) }; dumpEvents(events, expectedEvents); QVERIFY(events == expectedEvents); } void UtEwsOAuth::refreshSuccessful() { EwsOAuth oAuth(nullptr, testEmail, testClientId, testReturnUri); QVERIFY(Mock::QWebEngineView::instance); QVERIFY(Mock::QOAuth2AuthorizationCodeFlow::instance); QStringList events; setUpOAuth(oAuth, events, QString(), QMap()); Mock::QWebEngineView::instance->setRedirectUri(Mock::QOAuth2AuthorizationCodeFlow::instance->redirectUri()); auto time = QDateTime::currentSecsSinceEpoch(); constexpr unsigned int tokenLifetime = 86399; constexpr unsigned int extTokenLifetime = 345599; QString tokenReplyData; setUpAccessFunction(refreshToken1); setUpTokenFunction(accessToken1, refreshToken1, idToken1, time, tokenLifetime, extTokenLifetime, tokenReplyData); Mock::QOAuth2AuthorizationCodeFlow::instance->setState(testState); const auto initStatus = performAuthAction(oAuth, 1000, [](EwsOAuth *oAuth) { oAuth->init(); return true; }); QVERIFY(initStatus == 1); const auto authStatus = performAuthAction(oAuth, 2000, [](EwsOAuth *oAuth) { return oAuth->authenticate(true); }); QVERIFY(authStatus == 0); const auto authUrlString = Mock::authUrlString(authUrl, testClientId, testReturnUri, testEmail, resource, testState); const QStringList expectedEvents = { Mock::requestWalletMapString(), Mock::modifyParamsAuthString(testClientId, testReturnUri, testState), Mock::authorizeWithBrowserString(authUrlString), Mock::loadWebPageString(authUrlString), Mock::interceptRequestString(authUrlString), Mock::interceptRequestBlockedString(false), Mock::interceptRequestString(testReturnUri + QStringLiteral("?code=") + QString::fromLatin1(QUrl::toPercentEncoding(refreshToken1))), Mock::interceptRequestBlockedString(true), Mock::authorizationCallbackReceivedString(refreshToken1), Mock::modifyParamsTokenString(testClientId, testReturnUri, refreshToken1), Mock::networkReplyFinishedString(tokenReplyData), Mock::replyDataCallbackString(tokenReplyData), Mock::tokenCallbackString(accessToken1, refreshToken1, idToken1, time, tokenLifetime, extTokenLifetime, resource) }; dumpEvents(events, expectedEvents); QVERIFY(events == expectedEvents); events.clear(); oAuth.notifyRequestAuthFailed(); const auto reauthStatus = performAuthAction(oAuth, 2000, [](EwsOAuth *oAuth) { return oAuth->authenticate(false); }); QVERIFY(reauthStatus == 0); const QStringList expectedEventsRefresh = { Mock::modifyParamsTokenString(testClientId, testReturnUri, refreshToken1), Mock::networkReplyFinishedString(tokenReplyData), Mock::replyDataCallbackString(tokenReplyData), Mock::tokenCallbackString(accessToken1, refreshToken1, idToken1, time, tokenLifetime, extTokenLifetime, resource) }; dumpEvents(events, expectedEvents); QVERIFY(events == expectedEventsRefresh); } QString UtEwsOAuth::formatJsonSorted(const QVariantMap &map) { QStringList keys = map.keys(); keys.sort(); QStringList elems; for (const auto &key : keys) { QString val = map[key].toString(); val.replace(QLatin1Char('"'), QStringLiteral("\\\"")); elems.append(QStringLiteral("\"%1\":\"%2\"").arg(key, val)); } return QStringLiteral("{") + elems.join(QLatin1Char(',')) + QStringLiteral("}"); } int UtEwsOAuth::performAuthAction(EwsOAuth &oAuth, int timeout, std::function actionFn) { QEventLoop loop; int status = -1; QTimer timer; connect(&oAuth, &EwsOAuth::authSucceeded, &timer, [&]() { qDebug() << "succeeded"; loop.exit(0); status = 0; }); connect(&oAuth, &EwsOAuth::authFailed, &timer, [&](const QString &msg) { qDebug() << "failed" << msg; loop.exit(1); status = 1; }); connect(&timer, &QTimer::timeout, &timer, [&]() { qDebug() << "timeout"; loop.exit(1); status = 1; }); timer.setSingleShot(true); timer.start(timeout); if (!actionFn(&oAuth)) { return -1; } if (status == -1) { status = loop.exec(); } return status; } void UtEwsOAuth::setUpAccessFunction(const QString &refreshToken) { Mock::QWebEngineView::instance->setAuthFunction([&](const QUrl &, QVariantMap &map){ map[QStringLiteral("code")] = QUrl::toPercentEncoding(refreshToken); }); } void UtEwsOAuth::setUpTokenFunction(const QString &accessToken, const QString &refreshToken, const QString &idToken, quint64 time, int tokenLifetime, int extTokenLifetime, QString &tokenReplyData) { Mock::QOAuth2AuthorizationCodeFlow::instance->setTokenFunction( [=, &tokenReplyData](QString &data, QMap &headers) { QVariantMap map; map[QStringLiteral("token_type")] = QStringLiteral("Bearer"); map[QStringLiteral("scope")] = QStringLiteral("ReadWrite.All"); map[QStringLiteral("expires_in")] = QString::number(tokenLifetime); map[QStringLiteral("ext_expires_in")] = QString::number(extTokenLifetime); map[QStringLiteral("expires_on")] = QString::number(time + tokenLifetime); map[QStringLiteral("not_before")] = QString::number(time); map[QStringLiteral("resource")] = resource; map[QStringLiteral("access_token")] = accessToken; map[QStringLiteral("refresh_token")] = refreshToken; map[QStringLiteral("foci")] = QStringLiteral("1"); map[QStringLiteral("id_token")] = idToken; tokenReplyData = formatJsonSorted(map); data = tokenReplyData; headers[Mock::QNetworkRequest::ContentTypeHeader] = QStringLiteral("application/json; charset=utf-8"); return Mock::QNetworkReply::NoError; }); } void UtEwsOAuth::dumpEvents(const QStringList &events, const QStringList &expectedEvents) { for (const auto &event : events) { qDebug() << "Got event:" << event; } if (events != expectedEvents) { for (const auto &event : expectedEvents) { qDebug() << "Expected event:" << event; } } } -void UtEwsOAuth::setUpOAuth(EwsOAuth &oAuth, QStringList &events, QString password, QMap map) +void UtEwsOAuth::setUpOAuth(EwsOAuth &oAuth, QStringList &events, const QString &password, const QMap &map) { connect(Mock::QWebEngineView::instance.data(), &Mock::QWebEngineView::logEvent, this, [&events](const QString &event) { events.append(event); }); connect(Mock::QOAuth2AuthorizationCodeFlow::instance.data(), &Mock::QOAuth2AuthorizationCodeFlow::logEvent, this, [&events](const QString &event) { events.append(event); }); connect(&oAuth, &EwsOAuth::requestWalletPassword, this, [&oAuth, &events, password](bool) { events.append(QStringLiteral("RequestWalletPassword")); oAuth.walletPasswordRequestFinished(password); }); connect(&oAuth, &EwsOAuth::requestWalletMap, this, [&oAuth, &events, map]() { events.append(QStringLiteral("RequestWalletMap")); oAuth.walletMapRequestFinished(map); }); } QTEST_MAIN(UtEwsOAuth) #include "ewsoauth_ut.moc" diff --git a/resources/ews/test/unittests/ewsoauth_ut_mock.cpp b/resources/ews/test/unittests/ewsoauth_ut_mock.cpp index 2b6d1b2d2..67cdc325a 100644 --- a/resources/ews/test/unittests/ewsoauth_ut_mock.cpp +++ b/resources/ews/test/unittests/ewsoauth_ut_mock.cpp @@ -1,488 +1,488 @@ /* 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_ut_mock.h" #include #include #include Q_LOGGING_CATEGORY(EWSCLI_LOG, "org.kde.pim.ews.client", QtInfoMsg) namespace Mock { QPointer QWebEngineView::instance; QPointer QOAuth2AuthorizationCodeFlow::instance; QUrl QWebEngineUrlRequestJob::requestUrl() const { return mUrl; } QUrl QWebEngineUrlRequestInfo::requestUrl() const { return mUrl; } void QWebEngineUrlRequestInfo::block(bool) { mBlocked = true; } QWebEngineUrlRequestInterceptor::QWebEngineUrlRequestInterceptor(QObject *parent) : QObject(parent) { } QWebEngineUrlRequestInterceptor::~QWebEngineUrlRequestInterceptor() { } QWebEngineUrlSchemeHandler::QWebEngineUrlSchemeHandler(QObject *parent) : QObject(parent) { } QWebEngineUrlSchemeHandler::~QWebEngineUrlSchemeHandler() { } QWebEngineProfile::QWebEngineProfile(QObject *parent) : QObject(parent) , mInterceptor(nullptr) , mHandler(nullptr) { } QWebEngineProfile::~QWebEngineProfile() { } void QWebEngineProfile::setHttpUserAgent(const QString &ua) { mUserAgent = ua; } #if QTWEBENGINEWIDGETS_VERSION < QT_VERSION_CHECK(5, 13, 0) void QWebEngineProfile::setRequestInterceptor(QWebEngineUrlRequestInterceptor *interceptor) { mInterceptor = interceptor; } #else void QWebEngineProfile::setUrlRequestInterceptor(QWebEngineUrlRequestInterceptor *interceptor) { mInterceptor = interceptor; } #endif void QWebEngineProfile::installUrlSchemeHandler(QByteArray const &scheme, QWebEngineUrlSchemeHandler *handler) { mScheme = QString::fromLatin1(scheme); mHandler = handler; } QWebEnginePage::QWebEnginePage(QWebEngineProfile *profile, QObject *parent) : QObject(parent) , mProfile(profile) { connect(profile, &QWebEngineProfile::logEvent, this, &QWebEnginePage::logEvent); } QWebEnginePage::~QWebEnginePage() { } QWebEngineView::QWebEngineView(QWidget *parent) : QWidget(parent) , mPage(nullptr) { if (!instance) { instance = this; } else { qDebug() << "QWebEngineView instance already exists!"; } } QWebEngineView::~QWebEngineView() { } void QWebEngineView::load(const QUrl &url) { Q_EMIT logEvent(QStringLiteral("LoadWebPage:") + url.toString()); simulatePageLoad(url); QVariantMap params; if (mAuthFunction) { mAuthFunction(url, params); simulatePageLoad(QUrl(mRedirectUri + QStringLiteral("?") + QOAuth2AuthorizationCodeFlow::mapToSortedQuery(params).toString())); } else { qWarning() << "No authentication callback defined"; } } void QWebEngineView::simulatePageLoad(const QUrl &url) { if (mPage && mPage->mProfile && mPage->mProfile->mInterceptor) { QWebEngineUrlRequestInfo info(url, this); Q_EMIT logEvent(QStringLiteral("InterceptRequest:") + url.toString()); mPage->mProfile->mInterceptor->interceptRequest(info); Q_EMIT logEvent(QStringLiteral("InterceptRequestBlocked:%1").arg(info.mBlocked)); } else { qWarning() << "Cannot reach to request interceptor"; } } void QWebEngineView::setPage(QWebEnginePage *page) { mPage = page; connect(page, &QWebEnginePage::logEvent, this, &QWebEngineView::logEvent); } void QWebEngineView::stop() { } void QWebEngineView::setAuthFunction(const AuthFunc &func) { mAuthFunction = func; } void QWebEngineView::setRedirectUri(const QString &uri) { mRedirectUri = uri; } QNetworkReply::NetworkError QNetworkReply::error() const { return NoError; } QVariant QNetworkReply::header(QNetworkRequest::KnownHeaders header) const { return mHeaders[header]; } QAbstractOAuthReplyHandler::QAbstractOAuthReplyHandler(QObject *parent) : QObject(parent) { } QAbstractOAuthReplyHandler::~QAbstractOAuthReplyHandler() { } QAbstractOAuth::QAbstractOAuth(QObject *parent) : QObject(parent) , mStatus(Status::NotAuthenticated) { } void QAbstractOAuth::setReplyHandler(QAbstractOAuthReplyHandler *handler) { mReplyHandler = handler; } void QAbstractOAuth::setAuthorizationUrl(const QUrl &url) { mAuthUrl = url; } void QAbstractOAuth::setClientIdentifier(const QString &identifier) { mClientId = identifier; } void QAbstractOAuth::setModifyParametersFunction(const std::function *)> &func) { mModifyParamsFunc = func; } QString QAbstractOAuth::token() const { return mToken; } void QAbstractOAuth::setToken(const QString &token) { mToken = token; } QAbstractOAuth::Status QAbstractOAuth::status() const { return mStatus; } QAbstractOAuth2::QAbstractOAuth2(QObject *parent) : QAbstractOAuth(parent) { } QString QAbstractOAuth2::refreshToken() const { return mRefreshToken; } void QAbstractOAuth2::setRefreshToken(const QString &token) { mRefreshToken = token; } QOAuth2AuthorizationCodeFlow::QOAuth2AuthorizationCodeFlow(QObject *parent) : QAbstractOAuth2(parent) { if (!instance) { instance = this; } else { qDebug() << "QOAuth2AuthorizationCodeFlow instance already exists!"; } } QOAuth2AuthorizationCodeFlow::~QOAuth2AuthorizationCodeFlow() { } void QOAuth2AuthorizationCodeFlow::setAccessTokenUrl(const QUrl &url) { mTokenUrl = url; } void QOAuth2AuthorizationCodeFlow::grant() { QMap map; map[QStringLiteral("response_type")] = QStringLiteral("code"); map[QStringLiteral("client_id")] = QUrl::toPercentEncoding(mClientId); map[QStringLiteral("redirect_uri")] = QUrl::toPercentEncoding(mReplyHandler->callback()); map[QStringLiteral("scope")] = QString(); map[QStringLiteral("state")] = mState; Q_EMIT logEvent(QStringLiteral("ModifyParams:RequestingAuthorization:") + mapToSortedQuery(map).toString()); if (mModifyParamsFunc) { mModifyParamsFunc(Stage::RequestingAuthorization, &map); } mResource = QUrl::fromPercentEncoding(map[QStringLiteral("resource")].toByteArray()); QUrl url(mAuthUrl); url.setQuery(mapToSortedQuery(map)); Q_EMIT logEvent(QStringLiteral("AuthorizeWithBrowser:") + url.toString()); connect(this, &QAbstractOAuth2::authorizationCallbackReceived, this, &QOAuth2AuthorizationCodeFlow::authCallbackReceived, Qt::UniqueConnection); Q_EMIT authorizeWithBrowser(url); } void QOAuth2AuthorizationCodeFlow::refreshAccessToken() { mStatus = Status::RefreshingToken; doRefreshAccessToken(); } void QOAuth2AuthorizationCodeFlow::doRefreshAccessToken() { QMap map; map[QStringLiteral("grant_type")] = QStringLiteral("authorization_code"); map[QStringLiteral("code")] = QUrl::toPercentEncoding(mRefreshToken); map[QStringLiteral("client_id")] = QUrl::toPercentEncoding(mClientId); map[QStringLiteral("redirect_uri")] = QUrl::toPercentEncoding(mReplyHandler->callback()); Q_EMIT logEvent(QStringLiteral("ModifyParams:RequestingAccessToken:") + mapToSortedQuery(map).toString()); if (mModifyParamsFunc) { mModifyParamsFunc(Stage::RequestingAccessToken, &map); } connect(mReplyHandler, &QAbstractOAuthReplyHandler::tokensReceived, this, &QOAuth2AuthorizationCodeFlow::tokenCallbackReceived, Qt::UniqueConnection); connect(mReplyHandler, &QAbstractOAuthReplyHandler::replyDataReceived, this, &QOAuth2AuthorizationCodeFlow::replyDataCallbackReceived, Qt::UniqueConnection); if (mTokenFunc) { QNetworkReply reply(this); QString data; reply.mError = mTokenFunc(data, reply.mHeaders); reply.setData(data.toUtf8()); reply.open(QIODevice::ReadOnly); Q_EMIT logEvent(QStringLiteral("NetworkReplyFinished:") + data); mReplyHandler->networkReplyFinished(&reply); } else { qWarning() << "No token function defined"; } } QUrlQuery QOAuth2AuthorizationCodeFlow::mapToSortedQuery(QMap const &map) { QUrlQuery query; QStringList keys = map.keys(); keys.sort(); - for (const auto &key : keys) { + for (const auto &key : qAsConst(keys)) { query.addQueryItem(key, map[key].toString()); } return query; } void QOAuth2AuthorizationCodeFlow::authCallbackReceived(QMap const ¶ms) { Q_EMIT logEvent(QStringLiteral("AuthorizatioCallbackReceived:") + mapToSortedQuery(params).toString()); mRefreshToken = params[QStringLiteral("code")].toString(); if (!mRefreshToken.isEmpty()) { mStatus = Status::TemporaryCredentialsReceived; doRefreshAccessToken(); } else { Q_EMIT error(QString(), QString(), QUrl()); } } void QOAuth2AuthorizationCodeFlow::tokenCallbackReceived(const QVariantMap &tokens) { Q_EMIT logEvent(QStringLiteral("TokenCallback:") + mapToSortedQuery(tokens).toString()); mToken = tokens[QStringLiteral("access_token")].toString(); mRefreshToken = tokens[QStringLiteral("refresh_token")].toString(); mStatus = Status::Granted; Q_EMIT granted(); } void QOAuth2AuthorizationCodeFlow::replyDataCallbackReceived(const QByteArray &data) { Q_EMIT logEvent(QStringLiteral("ReplyDataCallback:") + QString::fromLatin1(data)); } QString QOAuth2AuthorizationCodeFlow::redirectUri() const { return mReplyHandler->callback(); } void QOAuth2AuthorizationCodeFlow::setTokenFunction(const TokenFunc &func) { mTokenFunc = func; } void QOAuth2AuthorizationCodeFlow::setState(const QString &state) { mState = state; } QString browserDisplayRequestString() { return QStringLiteral("BrowserDisplayRequest"); } QString modifyParamsAuthString(const QString &clientId, const QString &returnUri, const QString &state) { return QStringLiteral("ModifyParams:RequestingAuthorization:client_id=%1&redirect_uri=%2&response_type=code&scope&state=%3") .arg(QString::fromUtf8(QUrl::toPercentEncoding(clientId)), QString::fromLatin1(QUrl::toPercentEncoding(returnUri)), QString::fromLatin1(QUrl::toPercentEncoding(state))); } QString authUrlString(const QString &authUrl, const QString &clientId, const QString &returnUri, const QString &email, const QString &resource, const QString &state) { return QStringLiteral("%1?client_id=%2&login_hint=%3&prompt=login&redirect_uri=%4&resource=%5&response_type=code&scope&state=%6") .arg(authUrl, QString::fromLatin1(QUrl::toPercentEncoding(clientId)), email, QString::fromLatin1(QUrl::toPercentEncoding(returnUri)), QString::fromLatin1(QUrl::toPercentEncoding(resource)), QString::fromLatin1(QUrl::toPercentEncoding(state))); } QString authorizeWithBrowserString(const QString &url) { return QStringLiteral("AuthorizeWithBrowser:") + url; } QString loadWebPageString(const QString &url) { return QStringLiteral("LoadWebPage:") + url; } QString interceptRequestString(const QString &url) { return QStringLiteral("InterceptRequest:") + url; } QString interceptRequestBlockedString(bool blocked) { return QStringLiteral("InterceptRequestBlocked:%1").arg(blocked); } QString authorizationCallbackReceivedString(const QString &code) { return QStringLiteral("AuthorizatioCallbackReceived:code=%1").arg(code); } QString modifyParamsTokenString(const QString &clientId, const QString &returnUri, const QString &code) { return QStringLiteral("ModifyParams:RequestingAccessToken:client_id=%1&code=%2&grant_type=authorization_code&redirect_uri=%3") .arg(QString::fromUtf8(QUrl::toPercentEncoding(clientId)), QString::fromLatin1(QUrl::toPercentEncoding(code)), QString::fromLatin1(QUrl::toPercentEncoding(returnUri))); } QString networkReplyFinishedString(const QString &data) { return QStringLiteral("NetworkReplyFinished:") + data; } QString replyDataCallbackString(const QString &data) { return QStringLiteral("ReplyDataCallback:") + data; } QString tokenCallbackString(const QString &accessToken, const QString &refreshToken, const QString &idToken, quint64 time, unsigned int tokenLifetime, unsigned int extTokenLifetime, const QString &resource) { return QStringLiteral( "TokenCallback:access_token=%1&expires_in=%2&expires_on=%3&ext_expires_in=%4&foci=1&id_token=%5¬_before=%6&refresh_token=%7&resource=%8&scope=ReadWrite.All&token_type=Bearer") .arg(accessToken).arg(tokenLifetime).arg(time + tokenLifetime).arg(extTokenLifetime).arg(idToken).arg(time) .arg(refreshToken).arg(resource); } QString requestWalletMapString() { return QStringLiteral("RequestWalletMap"); } const QString &KJob::errorString() const { static const QString empty; return empty; } EwsPKeyAuthJob::EwsPKeyAuthJob(const QUrl &pkeyUri, const QString &certFile, const QString &keyFile, const QString &keyPassword, QObject *parent) : KJob(parent) { Q_UNUSED(pkeyUri); Q_UNUSED(certFile); Q_UNUSED(keyFile); Q_UNUSED(keyPassword); } const QUrl &EwsPKeyAuthJob::resultUri() const { static const QUrl empty; return empty; } } diff --git a/resources/ews/test/unittests/faketransferjob.cpp b/resources/ews/test/unittests/faketransferjob.cpp index ed724fbae..b436fccb0 100644 --- a/resources/ews/test/unittests/faketransferjob.cpp +++ b/resources/ews/test/unittests/faketransferjob.cpp @@ -1,74 +1,74 @@ /* Copyright (C) 2015-2017 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 "faketransferjob.h" #include #include QQueue FakeTransferJob::mVerifierQueue; -FakeTransferJob::FakeTransferJob(const QByteArray &postData, VerifierFn fn, QObject *parent) +FakeTransferJob::FakeTransferJob(const QByteArray &postData, const VerifierFn &fn, QObject *parent) : KIO::SpecialJob(QUrl(QStringLiteral("file:///tmp/")), QByteArray()) , mPostData(postData) , mVerifier(fn) { Q_UNUSED(parent); metaObject()->invokeMethod(this, "callVerifier", Qt::QueuedConnection); } FakeTransferJob::~FakeTransferJob() { } void FakeTransferJob::callVerifier() { mVerifier(this, mPostData); } void FakeTransferJob::postResponse(const QByteArray &resp) { mResponse = resp; qRegisterMetaType(); metaObject()->invokeMethod(this, "doData", Qt::QueuedConnection, Q_ARG(QByteArray, mResponse)); metaObject()->invokeMethod(this, "doEmitResult", Qt::QueuedConnection); } void FakeTransferJob::doData(const QByteArray &resp) { Q_EMIT data(this, resp); } void FakeTransferJob::doEmitResult() { emitResult(); } -void FakeTransferJob::addVerifier(QObject *obj, VerifierFn fn) +void FakeTransferJob::addVerifier(QObject *obj, const VerifierFn &fn) { Verifier vfy = {obj, fn}; mVerifierQueue.enqueue(vfy); } FakeTransferJob::Verifier FakeTransferJob::getVerifier() { return mVerifierQueue.dequeue(); } diff --git a/resources/ews/test/unittests/faketransferjob.h b/resources/ews/test/unittests/faketransferjob.h index 136103bb6..523721f45 100644 --- a/resources/ews/test/unittests/faketransferjob.h +++ b/resources/ews/test/unittests/faketransferjob.h @@ -1,84 +1,84 @@ /* Copyright (C) 2015-2017 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. */ #ifndef FAKETRANSFERJOB_H #define FAKETRANSFERJOB_H #include #include #include namespace KIO { class Job; } template struct Finally { Finally(F f) : cleanupf{f} { } ~Finally() { cleanupf(); } F cleanupf; }; template Finally finally(F f) { return Finally(f); } class FakeTransferJob : public KIO::SpecialJob { Q_OBJECT public: typedef std::function VerifierFn; struct Verifier { QObject *object; VerifierFn fn; }; - FakeTransferJob(const QByteArray &postData, VerifierFn fn, QObject *parent = nullptr); + FakeTransferJob(const QByteArray &postData, const VerifierFn &fn, QObject *parent = nullptr); ~FakeTransferJob(); - static void addVerifier(QObject *obj, VerifierFn fn); + static void addVerifier(QObject *obj, const VerifierFn &fn); static Verifier getVerifier(); public Q_SLOTS: void postResponse(const QByteArray &resp); private Q_SLOTS: void callVerifier(); void doEmitResult(); void doData(const QByteArray &resp); Q_SIGNALS: void requestReceived(FakeTransferJob *job, const QByteArray &req); private: QByteArray mPostData; QByteArray mResponse; VerifierFn mVerifier; static QQueue mVerifierQueue; }; #endif diff --git a/resources/google/calendar/calendarresource.cpp b/resources/google/calendar/calendarresource.cpp index 1ae92f65e..cca705a80 100644 --- a/resources/google/calendar/calendarresource.cpp +++ b/resources/google/calendar/calendarresource.cpp @@ -1,796 +1,794 @@ /* Copyright (C) 2011-2013 Daniel Vrátil 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 "calendarresource.h" #include "defaultreminderattribute.h" #include "settings.h" #include "settingsdialog.h" #include "googlecalendarresource_debug.h" #include "common/kgapiversionattribute.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define ROOT_COLLECTION_REMOTEID QStringLiteral("RootCollection") #define CALENDARS_PROPERTY "_KGAPI2CalendarPtr" #define TASK_PROPERTY "_KGAPI2::TaskPtr" Q_DECLARE_METATYPE(KGAPI2::ObjectsList) Q_DECLARE_METATYPE(KGAPI2::TaskPtr) namespace { static constexpr uint32_t KGAPIEventVersion = 1; } using namespace Akonadi; using namespace KGAPI2; CalendarResource::CalendarResource(const QString &id) : GoogleResource(id) { AttributeFactory::registerAttribute< DefaultReminderAttribute >(); AttributeFactory::registerAttribute(); updateResourceName(); } CalendarResource::~CalendarResource() { } GoogleSettings *CalendarResource::settings() const { return Settings::self(); } int CalendarResource::runConfigurationDialog(WId windowId) { QScopedPointer settingsDialog(new SettingsDialog(accountManager(), windowId, this)); settingsDialog->setWindowIcon(QIcon::fromTheme(QStringLiteral("im-google"))); return settingsDialog->exec(); } void CalendarResource::updateResourceName() { const QString accountName = Settings::self()->account(); setName(i18nc("%1 is account name (user@gmail.com)", "Google Calendars and Tasks (%1)", accountName.isEmpty() ? i18n("not configured") : accountName)); } QList< QUrl > CalendarResource::scopes() const { const QList scopes = { Account::calendarScopeUrl(), Account::tasksScopeUrl()}; return scopes; } void CalendarResource::retrieveItems(const Akonadi::Collection &collection) { if (!canPerformTask()) { return; } // https://bugs.kde.org/show_bug.cgi?id=308122: we can only request changes in // max. last 25 days, otherwise we get an error. int lastSyncDelta = -1; if (!collection.remoteRevision().isEmpty()) { lastSyncDelta = QDateTime::currentDateTimeUtc().toSecsSinceEpoch() - collection.remoteRevision().toULongLong(); } KGAPI2::Job *job = nullptr; if (collection.contentMimeTypes().contains(KCalendarCore::Event::eventMimeType())) { if (!collection.hasAttribute() || collection.attribute()->version() != KGAPIEventVersion) { lastSyncDelta = -1; } EventFetchJob *fetchJob = new EventFetchJob(collection.remoteId(), account(), this); if (lastSyncDelta > -1 && lastSyncDelta < 25 * 24 * 3600) { fetchJob->setFetchOnlyUpdated(collection.remoteRevision().toULongLong()); } if (!Settings::self()->eventsSince().isEmpty()) { const QDate date = QDate::fromString(Settings::self()->eventsSince(), Qt::ISODate); #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) fetchJob->setTimeMin(QDateTime(date).toSecsSinceEpoch()); #else fetchJob->setTimeMin(QDateTime(date.startOfDay()).toSecsSinceEpoch()); #endif } job = fetchJob; } else if (collection.contentMimeTypes().contains(KCalendarCore::Todo::todoMimeType())) { TaskFetchJob *fetchJob = new TaskFetchJob(collection.remoteId(), account(), this); if (lastSyncDelta > -1 && lastSyncDelta < 25 * 25 * 3600) { fetchJob->setFetchOnlyUpdated(collection.remoteRevision().toULongLong()); } job = fetchJob; } else { itemsRetrieved(Item::List()); return; } job->setProperty(COLLECTION_PROPERTY, QVariant::fromValue(collection)); connect(job, &KGAPI2::Job::progress, this, &CalendarResource::emitPercent); connect(job, &KGAPI2::Job::finished, this, &CalendarResource::slotItemsRetrieved); } void CalendarResource::retrieveCollections() { if (!canPerformTask()) { return; } CalendarFetchJob *fetchJob = new CalendarFetchJob(account(), this); connect(fetchJob, &EventFetchJob::finished, this, &CalendarResource::slotCalendarsRetrieved); } void CalendarResource::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) { if ((!collection.contentMimeTypes().contains(KCalendarCore::Event::eventMimeType()) && !collection.contentMimeTypes().contains(KCalendarCore::Todo::todoMimeType())) || (!canPerformTask(item, KCalendarCore::Event::eventMimeType()) && !canPerformTask(item, KCalendarCore::Todo::todoMimeType()))) { return; } if (collection.parentCollection() == Akonadi::Collection::root()) { cancelTask(i18n("The top-level collection cannot contain any tasks or events")); return; } KGAPI2::Job *job = nullptr; if (item.hasPayload()) { KCalendarCore::Event::Ptr event = item.payload(); EventPtr kevent(new Event(*event)); auto cjob = new EventCreateJob(kevent, collection.remoteId(), account(), this); cjob->setSendUpdates(SendUpdatesPolicy::None); job = cjob; } else if (item.hasPayload()) { KCalendarCore::Todo::Ptr todo = item.payload(); TaskPtr ktodo(new Task(*todo)); ktodo->setUid(QLatin1String("")); if (!ktodo->relatedTo(KCalendarCore::Incidence::RelTypeParent).isEmpty()) { Akonadi::Item parentItem; parentItem.setGid(ktodo->relatedTo(KCalendarCore::Incidence::RelTypeParent)); ItemFetchJob *fetchJob = new ItemFetchJob(parentItem, this); fetchJob->setProperty(ITEM_PROPERTY, QVariant::fromValue(item)); fetchJob->setProperty(TASK_PROPERTY, QVariant::fromValue(ktodo)); connect(fetchJob, &ItemFetchJob::finished, this, &CalendarResource::slotTaskAddedSearchFinished); return; } else { job = new TaskCreateJob(ktodo, collection.remoteId(), account(), this); } } else { cancelTask(i18n("Invalid payload type")); return; } job->setProperty(ITEM_PROPERTY, QVariant::fromValue(item)); connect(job, &CreateJob::finished, this, &CalendarResource::slotCreateJobFinished); } void CalendarResource::itemChanged(const Akonadi::Item &item, const QSet< QByteArray > &partIdentifiers) { Q_UNUSED(partIdentifiers); if (!canPerformTask(item, KCalendarCore::Event::eventMimeType()) && !canPerformTask(item, KCalendarCore::Todo::todoMimeType())) { return; } KGAPI2::Job *job = nullptr; if (item.hasPayload()) { KCalendarCore::Event::Ptr event = item.payload(); EventPtr kevent(new Event(*event)); auto mjob = new EventModifyJob(kevent, item.parentCollection().remoteId(), account(), this); mjob->setSendUpdates(SendUpdatesPolicy::None); connect(mjob, &EventModifyJob::finished, this, &CalendarResource::slotGenericJobFinished); job = mjob; } else if (item.hasPayload()) { KCalendarCore::Todo::Ptr todo = item.payload(); //FIXME unused ktodo ? //TaskPtr ktodo(new Task(*todo)); QString parentUid = todo->relatedTo(KCalendarCore::Incidence::RelTypeParent); job = new TaskMoveJob(item.remoteId(), item.parentCollection().remoteId(), parentUid, account(), this); job->setProperty(ITEM_PROPERTY, QVariant::fromValue(item)); connect(job, &TaskMoveJob::finished, this, &CalendarResource::slotModifyTaskReparentFinished); } else { cancelTask(i18n("Invalid payload type")); return; } job->setProperty(ITEM_PROPERTY, QVariant::fromValue(item)); } void CalendarResource::itemRemoved(const Akonadi::Item &item) { if (!canPerformTask()) { return; } if (item.mimeType() == KCalendarCore::Event::eventMimeType()) { KGAPI2::Job *job = new EventDeleteJob(item.remoteId(), item.parentCollection().remoteId(), account(), this); job->setProperty(ITEM_PROPERTY, QVariant::fromValue(item)); connect(job, &EventDeleteJob::finished, this, &CalendarResource::slotGenericJobFinished); } else if (item.mimeType() == KCalendarCore::Todo::todoMimeType()) { /* Google always automatically removes tasks with all their subtasks. In KOrganizer * by default we only remove the item we are given. For this reason we have to first * fetch all tasks, find all sub-tasks for the task being removed and detach them * from the task. Only then the task can be safely removed. */ ItemFetchJob *fetchJob = new ItemFetchJob(item.parentCollection()); fetchJob->setAutoDelete(true); fetchJob->fetchScope().fetchFullPayload(true); fetchJob->setProperty(ITEM_PROPERTY, QVariant::fromValue(item)); connect(fetchJob, &ItemFetchJob::finished, this, &CalendarResource::slotRemoveTaskFetchJobFinished); fetchJob->start(); } else { cancelTask(i18n("Invalid payload type. Expected event or todo, got %1", item.mimeType())); } } void CalendarResource::itemMoved(const Item &item, const Collection &collectionSource, const Collection &collectionDestination) { if (!canPerformTask()) { return; } if (collectionDestination.parentCollection() == Akonadi::Collection::root()) { cancelTask(i18n("The top-level collection cannot contain any tasks or events")); return; } if (item.mimeType() != KCalendarCore::Event::eventMimeType()) { cancelTask(i18n("Moving tasks between task lists is not supported")); return; } KGAPI2::Job *job = new EventMoveJob(item.remoteId(), collectionSource.remoteId(), collectionDestination.remoteId(), account(), this); job->setProperty(ITEM_PROPERTY, QVariant::fromValue(item)); connect(job, &EventMoveJob::finished, this, &CalendarResource::slotGenericJobFinished); } void CalendarResource::collectionAdded(const Collection &collection, const Collection &parent) { Q_UNUSED(parent) if (!canPerformTask()) { return; } KGAPI2::Job *job = nullptr; if (collection.contentMimeTypes().contains(KCalendarCore::Event::eventMimeType())) { CalendarPtr calendar(new Calendar()); calendar->setTitle(collection.displayName()); calendar->setEditable(true); job = new CalendarCreateJob(calendar, account(), this); } if (collection.contentMimeTypes().contains(KCalendarCore::Todo::todoMimeType())) { TaskListPtr taskList(new TaskList()); taskList->setTitle(collection.displayName()); job = new TaskListCreateJob(taskList, account(), this); } else { cancelTask(i18n("Unknown collection mimetype")); return; } job->setProperty(COLLECTION_PROPERTY, QVariant::fromValue(collection)); connect(job, &KGAPI2::Job::finished, this, &CalendarResource::slotGenericJobFinished); } void CalendarResource::collectionChanged(const Collection &collection) { if (!canPerformTask()) { return; } KGAPI2::Job *job = nullptr; if (collection.contentMimeTypes().contains(KCalendarCore::Event::eventMimeType())) { CalendarPtr calendar(new Calendar()); calendar->setUid(collection.remoteId()); calendar->setTitle(collection.displayName()); calendar->setEditable(true); job = new CalendarModifyJob(calendar, account(), this); } if (collection.contentMimeTypes().contains(KCalendarCore::Todo::todoMimeType())) { TaskListPtr taskList(new TaskList()); taskList->setUid(collection.remoteId()); taskList->setTitle(collection.displayName()); job = new TaskListModifyJob(taskList, account(), this); } else { cancelTask(i18n("Unknown collection mimetype")); return; } job->setProperty(COLLECTION_PROPERTY, QVariant::fromValue(collection)); connect(job, &KGAPI2::Job::finished, this, &CalendarResource::slotGenericJobFinished); } void CalendarResource::collectionRemoved(const Collection &collection) { if (!canPerformTask()) { return; } KGAPI2::Job *job = nullptr; if (collection.contentMimeTypes().contains(KCalendarCore::Event::eventMimeType())) { job = new CalendarDeleteJob(collection.remoteId(), account(), this); } else if (collection.contentMimeTypes().contains(KCalendarCore::Todo::todoMimeType())) { job = new TaskListDeleteJob(collection.remoteId(), account(), this); } else { cancelTask(i18n("Unknown collection mimetype")); return; } job->setProperty(COLLECTION_PROPERTY, QVariant::fromValue(collection)); connect(job, &KGAPI2::Job::finished, this, &CalendarResource::slotGenericJobFinished); } void CalendarResource::slotCalendarsRetrieved(KGAPI2::Job *job) { if (!handleError(job)) { return; } CalendarFetchJob *calendarJob = qobject_cast(job); ObjectsList objects = calendarJob->items(); calendarJob->deleteLater(); TaskListFetchJob *fetchJob = new TaskListFetchJob(job->account(), this); fetchJob->setProperty(CALENDARS_PROPERTY, QVariant::fromValue(objects)); connect(fetchJob, &EventFetchJob::finished, this, &CalendarResource::slotCollectionsRetrieved); } void CalendarResource::slotCollectionsRetrieved(KGAPI2::Job *job) { if (!handleError(job)) { return; } TaskListFetchJob *fetchJob = qobject_cast(job); ObjectsList calendars = fetchJob->property(CALENDARS_PROPERTY).value(); ObjectsList taskLists = fetchJob->items(); CachePolicy cachePolicy; if (Settings::self()->enableIntervalCheck()) { cachePolicy.setInheritFromParent(false); cachePolicy.setIntervalCheckTime(Settings::self()->intervalCheckTime()); } m_rootCollection = Collection(); m_rootCollection.setContentMimeTypes(QStringList() << Collection::mimeType()); m_rootCollection.setRemoteId(ROOT_COLLECTION_REMOTEID); m_rootCollection.setName(fetchJob->account()->accountName()); m_rootCollection.setParentCollection(Collection::root()); m_rootCollection.setRights(Collection::CanCreateCollection); m_rootCollection.setCachePolicy(cachePolicy); EntityDisplayAttribute *attr = m_rootCollection.attribute(Collection::AddIfMissing); attr->setDisplayName(fetchJob->account()->accountName()); attr->setIconName(QStringLiteral("im-google")); m_collections[ ROOT_COLLECTION_REMOTEID ] = m_rootCollection; const QStringList activeCalendars = Settings::self()->calendars(); - for (const ObjectPtr &object : calendars) { + for (const ObjectPtr &object : qAsConst(calendars)) { const CalendarPtr &calendar = object.dynamicCast(); if (!activeCalendars.contains(calendar->uid())) { continue; } Collection collection; collection.setContentMimeTypes(QStringList() << KCalendarCore::Event::eventMimeType()); collection.setName(calendar->uid()); collection.setParentCollection(m_rootCollection); collection.setRemoteId(calendar->uid()); if (calendar->editable()) { collection.setRights(Collection::CanChangeCollection |Collection::CanCreateItem |Collection::CanChangeItem |Collection::CanDeleteItem); } else { collection.setRights(Collection::ReadOnly); } EntityDisplayAttribute *attr = collection.attribute(Collection::AddIfMissing); attr->setDisplayName(calendar->title()); attr->setIconName(QStringLiteral("view-calendar")); auto colorAttr = collection.attribute(Collection::AddIfMissing); colorAttr->setColor(calendar->backgroundColor()); DefaultReminderAttribute *reminderAttr = collection.attribute(Collection::AddIfMissing); reminderAttr->setReminders(calendar->defaultReminders()); // Block email reminders, since Google sends them for us BlockAlarmsAttribute *blockAlarms = collection.attribute(Collection::AddIfMissing); blockAlarms->blockAlarmType(KCalendarCore::Alarm::Audio, false); blockAlarms->blockAlarmType(KCalendarCore::Alarm::Display, false); blockAlarms->blockAlarmType(KCalendarCore::Alarm::Procedure, false); m_collections[ collection.remoteId() ] = collection; } const QStringList activeTaskLists = Settings::self()->taskLists(); - for (const ObjectPtr &object : taskLists) { + for (const ObjectPtr &object : qAsConst(taskLists)) { const TaskListPtr &taskList = object.dynamicCast(); if (!activeTaskLists.contains(taskList->uid())) { continue; } Collection collection; collection.setContentMimeTypes(QStringList() << KCalendarCore::Todo::todoMimeType()); collection.setName(taskList->uid()); collection.setParentCollection(m_rootCollection); collection.setRemoteId(taskList->uid()); collection.setRights(Collection::CanChangeCollection |Collection::CanCreateItem |Collection::CanChangeItem |Collection::CanDeleteItem); EntityDisplayAttribute *attr = collection.attribute(Collection::AddIfMissing); attr->setDisplayName(taskList->title()); attr->setIconName(QStringLiteral("view-pim-tasks")); m_collections[ collection.remoteId() ] = collection; } collectionsRetrieved(Akonadi::valuesToVector(m_collections)); job->deleteLater(); } void CalendarResource::slotItemsRetrieved(KGAPI2::Job *job) { if (!handleError(job)) { return; } Item::List changedItems, removedItems; Collection collection = job->property(COLLECTION_PROPERTY).value(); DefaultReminderAttribute *attr = collection.attribute(); bool isIncremental = false; ObjectsList objects = qobject_cast(job)->items(); if (collection.contentMimeTypes().contains(KCalendarCore::Event::eventMimeType())) { - QMap< QString, EventPtr > recurrentEvents; - isIncremental = (qobject_cast(job)->fetchOnlyUpdated() > 0); Q_FOREACH (const ObjectPtr &object, objects) { EventPtr event = object.dynamicCast(); if (event->useDefaultReminders() && attr) { const KCalendarCore::Alarm::List alarms = attr->alarms(event.data()); for (const KCalendarCore::Alarm::Ptr &alarm : alarms) { event->addAlarm(alarm); } } Item item; item.setMimeType(KCalendarCore::Event::eventMimeType()); item.setParentCollection(collection); item.setRemoteId(event->id()); item.setRemoteRevision(event->etag()); item.setPayload(event.dynamicCast()); if (event->deleted()) { removedItems << item; } else { changedItems << item; } } if (!isIncremental) { collection.attribute(Collection::AddIfMissing)->setVersion(KGAPIEventVersion); } } else if (collection.contentMimeTypes().contains(KCalendarCore::Todo::todoMimeType())) { isIncremental = (qobject_cast(job)->fetchOnlyUpdated() > 0); Q_FOREACH (const ObjectPtr &object, objects) { TaskPtr task = object.dynamicCast(); Item item; item.setMimeType(KCalendarCore::Todo::todoMimeType()); item.setParentCollection(collection); item.setRemoteId(task->uid()); item.setRemoteRevision(task->etag()); item.setPayload(task.dynamicCast()); if (task->deleted()) { removedItems << item; } else { changedItems << item; } } } if (isIncremental) { itemsRetrievedIncremental(changedItems, removedItems); } else { itemsRetrieved(changedItems); } const QDateTime local(QDateTime::currentDateTime()); const QDateTime UTC(local.toUTC()); collection.setRemoteRevision(QString::number(UTC.toSecsSinceEpoch())); new CollectionModifyJob(collection, this); job->deleteLater(); } void CalendarResource::slotModifyTaskReparentFinished(KGAPI2::Job *job) { if (!handleError(job)) { return; } Item item = job->property(ITEM_PROPERTY).value(); KCalendarCore::Todo::Ptr todo = item.payload(); TaskPtr ktodo(new Task(*todo.data())); job = new TaskModifyJob(ktodo, item.parentCollection().remoteId(), job->account(), this); job->setProperty(ITEM_PROPERTY, QVariant::fromValue(item)); connect(job, &KGAPI2::Job::finished, this, &CalendarResource::slotGenericJobFinished); } void CalendarResource::slotRemoveTaskFetchJobFinished(KJob *job) { if (job->error()) { cancelTask(i18n("Failed to delete task: %1", job->errorString())); return; } ItemFetchJob *fetchJob = qobject_cast(job); Item removedItem = fetchJob->property(ITEM_PROPERTY).value(); Item::List detachItems; const Item::List items = fetchJob->items(); for (Item item : items) { if (!item.hasPayload()) { qCDebug(GOOGLE_CALENDAR_LOG) << "Item " << item.remoteId() << " does not have Todo payload"; continue; } KCalendarCore::Todo::Ptr todo = item.payload(); /* If this item is child of the item we want to remove then add it to detach list */ if (todo->relatedTo(KCalendarCore::Incidence::RelTypeParent) == removedItem.remoteId()) { todo->setRelatedTo(QString(), KCalendarCore::Incidence::RelTypeParent); item.setPayload(todo); detachItems << item; } } /* If there are no items do detach, then delete the task right now */ if (detachItems.isEmpty()) { slotDoRemoveTask(job); return; } /* Send modify request to detach all the sub-tasks from the task that is about to be * removed. */ ItemModifyJob *modifyJob = new ItemModifyJob(detachItems); modifyJob->setProperty(ITEM_PROPERTY, QVariant::fromValue(removedItem)); modifyJob->setAutoDelete(true); connect(modifyJob, &ItemModifyJob::finished, this, &CalendarResource::slotDoRemoveTask); } void CalendarResource::slotDoRemoveTask(KJob *job) { if (job->error()) { cancelTask(i18n("Failed to delete task: %1", job->errorString())); return; } // Make sure account is still valid if (!canPerformTask()) { return; } Item item = job->property(ITEM_PROPERTY).value< Item >(); /* Now finally we can safely remove the task we wanted to */ TaskDeleteJob *deleteJob = new TaskDeleteJob(item.remoteId(), item.parentCollection().remoteId(), account(), this); deleteJob->setProperty(ITEM_PROPERTY, QVariant::fromValue(item)); connect(deleteJob, &TaskDeleteJob::finished, this, &CalendarResource::slotGenericJobFinished); } void CalendarResource::slotTaskAddedSearchFinished(KJob *job) { ItemFetchJob *fetchJob = qobject_cast(job); Item item = job->property(ITEM_PROPERTY).value(); TaskPtr task = job->property(TASK_PROPERTY).value(); Item::List items = fetchJob->items(); qCDebug(GOOGLE_CALENDAR_LOG) << "Parent query returned" << items.count() << "results"; const QString tasksListId = item.parentCollection().remoteId(); // Make sure account is still valid if (!canPerformTask()) { return; } KGAPI2::Job *newJob = nullptr; // The parent is not known, so give up and just store the item in Google // without the information about parent. if (items.isEmpty()) { task->setRelatedTo(QString(), KCalendarCore::Incidence::RelTypeParent); newJob = new TaskCreateJob(task, tasksListId, account(), this); } else { Item matchedItem = items.first(); task->setRelatedTo(matchedItem.remoteId(), KCalendarCore::Incidence::RelTypeParent); TaskCreateJob *createJob = new TaskCreateJob(task, tasksListId, account(), this); createJob->setParentItem(matchedItem.remoteId()); newJob = createJob; } newJob->setProperty(ITEM_PROPERTY, QVariant::fromValue(item)); connect(newJob, &KGAPI2::Job::finished, this, &CalendarResource::slotCreateJobFinished); } void CalendarResource::slotCreateJobFinished(KGAPI2::Job *job) { if (!handleError(job)) { return; } Item item = job->property(ITEM_PROPERTY).value(); CreateJob *createJob = qobject_cast(job); ObjectsList objects = createJob->items(); Q_ASSERT(objects.count() > 0); if (item.mimeType() == KCalendarCore::Event::eventMimeType()) { EventPtr event = objects.first().dynamicCast(); item.setRemoteId(event->id()); item.setRemoteRevision(event->etag()); item.setGid(event->uid()); changeCommitted(item); item.setPayload(event.dynamicCast()); new ItemModifyJob(item, this); } else if (item.mimeType() == KCalendarCore::Todo::todoMimeType()) { TaskPtr task = objects.first().dynamicCast(); item.setRemoteId(task->uid()); item.setRemoteRevision(task->etag()); item.setGid(task->uid()); changeCommitted(item); item.setPayload(task.dynamicCast()); new ItemModifyJob(item, this); } } QDateTime CalendarResource::lastCacheUpdate() const { return QDateTime(); } void CalendarResource::canHandleFreeBusy(const QString &email) const { if (!const_cast(this)->canPerformTask()) { handlesFreeBusy(email, false); return; } auto job = new KGAPI2::FreeBusyQueryJob(email, QDateTime::currentDateTimeUtc(), QDateTime::currentDateTimeUtc().addSecs(3600), const_cast(this)->account(), const_cast(this)); connect(job, &KGAPI2::Job::finished, this, &CalendarResource::slotCanHandleFreeBusyJobFinished); } void CalendarResource::slotCanHandleFreeBusyJobFinished(KGAPI2::Job *job) { auto queryJob = qobject_cast(job); if (!handleError(job, false)) { handlesFreeBusy(queryJob->id(), false); return; } handlesFreeBusy(queryJob->id(), true); } void CalendarResource::retrieveFreeBusy(const QString &email, const QDateTime &start, const QDateTime &end) { if (!const_cast(this)->canPerformTask()) { freeBusyRetrieved(email, QString(), false, QString()); return; } auto job = new KGAPI2::FreeBusyQueryJob(email, start, end, account(), this); connect(job, &KGAPI2::Job::finished, this, &CalendarResource::slotRetrieveFreeBusyJobFinished); } void CalendarResource::slotRetrieveFreeBusyJobFinished(KGAPI2::Job *job) { auto queryJob = qobject_cast(job); if (!handleError(job, false)) { freeBusyRetrieved(queryJob->id(), QString(), false, QString()); return; } KCalendarCore::FreeBusy::Ptr fb(new KCalendarCore::FreeBusy); fb->setUid(QStringLiteral("%1%2@google.com").arg(QDateTime::currentDateTimeUtc().toString(QStringLiteral("yyyyMMddTHHmmssZ")))); fb->setOrganizer(account()->accountName()); fb->addAttendee(KCalendarCore::Attendee(QString(), queryJob->id())); // FIXME: is it really sort? fb->setDateTime(QDateTime::currentDateTimeUtc(), KCalendarCore::IncidenceBase::RoleSort); Q_FOREACH (const KGAPI2::FreeBusyQueryJob::BusyRange &range, queryJob->busy()) { fb->addPeriod(range.busyStart, range.busyEnd); } KCalendarCore::ICalFormat format; const QString fbStr = format.createScheduleMessage(fb, KCalendarCore::iTIPRequest); freeBusyRetrieved(queryJob->id(), fbStr, true, QString()); } AKONADI_RESOURCE_MAIN(CalendarResource) diff --git a/resources/google/common/googleaccountmanager.h b/resources/google/common/googleaccountmanager.h index 8d6684b9b..7039c0352 100644 --- a/resources/google/common/googleaccountmanager.h +++ b/resources/google/common/googleaccountmanager.h @@ -1,70 +1,70 @@ /* * Copyright (C) 2013 Daniel Vrátil * * 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 2 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, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * */ #ifndef GOOGLEACCOUNTMANAGER_H #define GOOGLEACCOUNTMANAGER_H #include #include #include #include #include namespace KWallet { class Wallet; } class GoogleAccountManager : public QObject { Q_OBJECT public: explicit GoogleAccountManager(QObject *parent = nullptr); ~GoogleAccountManager() override; bool isReady() const; bool storeAccount(const KGAPI2::AccountPtr &account); KGAPI2::AccountPtr findAccount(const QString &accountName) const; bool removeAccount(const QString &accountName); KGAPI2::AccountsList listAccounts() const; void cleanup(const QString &accountName); Q_SIGNALS: void managerReady(bool ready); void accountAdded(const KGAPI2::AccountPtr &account); void accountChanged(const KGAPI2::AccountPtr &account); void accountRemoved(const QString &accountName); private Q_SLOTS: void initManager(); void slotWalletOpened(bool success); void slotWalletClosed(); void slotFolderUpdated(const QString &folder); - KGAPI2::AccountPtr findAccountInWallet(const QString &accountName) const; private: + KGAPI2::AccountPtr findAccountInWallet(const QString &accountName) const; bool m_isReady; QPointer m_wallet; mutable QMap m_accounts; }; #endif // GOOGLEACCOUNTMANAGER_H diff --git a/resources/kolab/kolabretrievetagstask.h b/resources/kolab/kolabretrievetagstask.h index 19c8549c4..7231badae 100644 --- a/resources/kolab/kolabretrievetagstask.h +++ b/resources/kolab/kolabretrievetagstask.h @@ -1,64 +1,64 @@ /* Copyright (c) 2014 Christian Mollekopf 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 KOLABRETRIEVETAGSTASK_H #define KOLABRETRIEVETAGSTASK_H #include "kolabrelationresourcetask.h" #include namespace Kolab { class KolabObjectReader; -class RelationMember; +struct RelationMember; } // namespace Kolab class KolabRetrieveTagTask : public KolabRelationResourceTask { Q_OBJECT public: enum RetrieveType { RetrieveTags, RetrieveRelations }; explicit KolabRetrieveTagTask(const ResourceStateInterface::Ptr &resource, RetrieveType type, QObject *parent = nullptr); protected: void startRelationTask(KIMAP::Session *session) override; private: KIMAP::Session *mSession = nullptr; Akonadi::Tag::List mTags; QHash mTagMembers; Akonadi::Relation::List mRelations; RetrieveType mRetrieveType; private Q_SLOTS: void onFinalSelectDone(KJob *job); void onMessagesAvailable(const QMap &messages); void onHeadersFetchDone(KJob *job); private: void extractTag(const Kolab::KolabObjectReader &reader, qint64 remoteUid); void extractRelation(const Kolab::KolabObjectReader &reader, qint64 remoteUid); Akonadi::Item extractMember(const Kolab::RelationMember &member); void taskComplete(); }; #endif // KOLABCHANGETAGTASK_H diff --git a/resources/kolab/pimkolab/kolabformat/errorhandler.h b/resources/kolab/pimkolab/kolabformat/errorhandler.h index c1e619f1a..03816b127 100644 --- a/resources/kolab/pimkolab/kolabformat/errorhandler.h +++ b/resources/kolab/pimkolab/kolabformat/errorhandler.h @@ -1,154 +1,155 @@ /* * Copyright (C) 2012 Christian Mollekopf * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ #ifndef ERRORHANDLER_H #define ERRORHANDLER_H #include "kolab_export.h" #include #include #include namespace Kolab { class DebugStream; /** * Kolab Error Handler * * Errors are reported during an operation, but the operation might still succeed. * The error handler therefore contains all errors which occurred during a single operation, * and must be cleared at the start of a new operation. * * A user of the kolabobject classes should check ErrorHandler::error() after every operation. * * all non-const functions are not for the user of this class and only exist for internal usage. * * TODO: Hide everything which is not meant for the user from the interface. * FIXME: Use Threadlocal storage to make this threadsafe. */ class KOLAB_EXPORT ErrorHandler { public: enum Severity { Debug, Warning, //Warning, error could be corrected, object can be used without dataloss. This warning is also used if dataloss is acceptable because a feature is explicitly not supported. Error, //Potentially corrupt object, writing the object back could result in dataloss. (Object could still be used to display the data readonly). Critical //Critical error, produced object cannot be used and should be thrown away (writing back will result in dataloss). }; struct Err { Err(Severity s, const QString &m, const QString &l) : severity(s) , message(m) , location(l) { } Severity severity; QString message; QString location; }; static ErrorHandler &instance() { static ErrorHandler inst; return inst; } void addError(Severity s, const QString &message, const QString &location); const QList &getErrors() const; Severity error() const; QString errorMessage() const; void clear(); /** * Check for errors during the libkolabxml reading/writing process and copy them into this error handler. */ static void handleLibkolabxmlErrors(); static void clearErrors() { ErrorHandler::instance().clear(); } static bool errorOccured() { if (ErrorHandler::instance().error() >= Error) { return true; } return false; } /** * Returns a debug stream to which logs errors */ static QDebug debugStream(Severity, int line, const char *file); private: ErrorHandler(); ErrorHandler(const ErrorHandler &); ErrorHandler &operator=(const ErrorHandler &); Severity m_worstError; QString m_worstErrorMessage; QList m_errorQueue; QScopedPointer m_debugStream; }; void logMessage(const QString &, const QString &, int, ErrorHandler::Severity s); #define LOG(message) logMessage(message, __FILE__, __LINE__, ErrorHandler::Debug); #define WARNING(message) logMessage(message, __FILE__, __LINE__, ErrorHandler::Warning); #define ERROR(message) logMessage(message, __FILE__, __LINE__, ErrorHandler::Error); #define CRITICAL(message) logMessage(message, QStringLiteral(__FILE__), __LINE__, ErrorHandler::Critical); class DebugStream : public QIODevice { + Q_OBJECT public: QString m_location; ErrorHandler::Severity m_severity; DebugStream(); ~DebugStream() override; bool isSequential() const override { return true; } qint64 readData(char *, qint64) override { return 0; /* eof */ } qint64 readLineData(char *, qint64) override { return 0; /* eof */ } qint64 writeData(const char *data, qint64 len) override; private: Q_DISABLE_COPY(DebugStream) }; #define Debug() Kolab::ErrorHandler::debugStream(Kolab::ErrorHandler::Debug, __LINE__, __FILE__) #define Warning() Kolab::ErrorHandler::debugStream(Kolab::ErrorHandler::Warning, __LINE__, __FILE__) #define Error() Kolab::ErrorHandler::debugStream(Kolab::ErrorHandler::Error, __LINE__, __FILE__) #define Critical() Kolab::ErrorHandler::debugStream(Kolab::ErrorHandler::Critical, __LINE__, __FILE__) } QDebug operator<<(QDebug dbg, const std::string &s); #endif // ERRORHANDLER_H diff --git a/resources/maildir/maildirresource.cpp b/resources/maildir/maildirresource.cpp index fc57ef2ef..c8b2ad0bf 100644 --- a/resources/maildir/maildirresource.cpp +++ b/resources/maildir/maildirresource.cpp @@ -1,886 +1,886 @@ /* Copyright (c) 2007 Till Adam 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 "maildirresource.h" #include "settings.h" #include "maildirsettingsadaptor.h" #include "configdialog.h" #include "retrieveitemsjob.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "maildirresource_debug.h" #include #include #include "libmaildir/maildir.h" #include using namespace Akonadi; using KPIM::Maildir; using namespace Akonadi_Maildir_Resource; #define CLEANER_TIMEOUT 2*6000 Maildir MaildirResource::maildirForCollection(const Collection &col) { const QString path = maildirPathForCollection(col); if (mMaildirsForCollection.contains(path)) { return mMaildirsForCollection.value(path); } if (col.remoteId().isEmpty()) { qCWarning(MAILDIRRESOURCE_LOG) << "Got incomplete ancestor chain:" << col; return Maildir(); } if (col.parentCollection() == Collection::root()) { if (col.remoteId() != mSettings->path()) { qCWarning(MAILDIRRESOURCE_LOG) << "RID mismatch, is " << col.remoteId() << " expected " << mSettings->path(); } Maildir maildir(col.remoteId(), mSettings->topLevelIsContainer()); mMaildirsForCollection.insert(path, maildir); return maildir; } Maildir parentMd = maildirForCollection(col.parentCollection()); Maildir maildir = parentMd.subFolder(col.remoteId()); mMaildirsForCollection.insert(path, maildir); return maildir; } Collection MaildirResource::collectionForMaildir(const Maildir &md) const { if (!md.isValid()) { return Collection(); } Collection col; if (md.path() == mSettings->path()) { col.setRemoteId(md.path()); col.setParentCollection(Collection::root()); } else { const Collection parent = collectionForMaildir(md.parent()); col.setRemoteId(md.name()); col.setParentCollection(parent); } return col; } MaildirResource::MaildirResource(const QString &id) : ResourceBase(id) , mSettings(new MaildirSettings(config())) , mFsWatcher(new KDirWatch(this)) { // we cannot be sure that a config file is existing // the MaildirResource will always be build // look for a resource of this name QString configFile = QStandardPaths::locate(QStandardPaths::ConfigLocation, id + QLatin1String("rc")); // if not present, create it if (configFile.isEmpty()) { // check if the resource was used before CollectionFetchJob *job = new CollectionFetchJob(Collection::root(), Akonadi::CollectionFetchJob::FirstLevel, this); job->fetchScope().setResource(id); connect(job, &CollectionFetchJob::result, this, &MaildirResource::attemptConfigRestoring); job->start(); } new MaildirSettingsAdaptor(mSettings); QDBusConnection::sessionBus().registerObject(QStringLiteral("/Settings"), mSettings, QDBusConnection::ExportAdaptors); connect(this, &MaildirResource::reloadConfiguration, this, &MaildirResource::configurationChanged); // We need to enable this here, otherwise we neither get the remote ID of the // parent collection when a collection changes, nor the full item when an item // is added. changeRecorder()->fetchCollection(true); changeRecorder()->itemFetchScope().fetchFullPayload(true); changeRecorder()->itemFetchScope().setAncestorRetrieval(ItemFetchScope::All); changeRecorder()->itemFetchScope().setFetchModificationTime(false); changeRecorder()->collectionFetchScope().setAncestorRetrieval(CollectionFetchScope::All); changeRecorder()->fetchChangedOnly(true); setHierarchicalRemoteIdentifiersEnabled(true); ItemFetchScope scope(changeRecorder()->itemFetchScope()); scope.fetchFullPayload(false); scope.fetchPayloadPart(MessagePart::Header); scope.setAncestorRetrieval(ItemFetchScope::None); setItemSynchronizationFetchScope(scope); connect(mFsWatcher, &KDirWatch::dirty, this, &MaildirResource::slotDirChanged); if (!ensureSaneConfiguration()) { Q_EMIT error(i18n("Unusable configuration.")); } else { synchronizeCollectionTree(); } mChangedCleanerTimer = new QTimer(this); connect(mChangedCleanerTimer, &QTimer::timeout, this, &MaildirResource::changedCleaner); } void MaildirResource::attemptConfigRestoring(KJob *job) { if (job->error()) { qCDebug(MAILDIRRESOURCE_LOG) << job->errorString(); return; } // we cannot be sure that a config file is existing const QString id = identifier(); const QString configFile = QStandardPaths::locate(QStandardPaths::ConfigLocation, id + QLatin1String("rc")); // we test it again, to be sure if (configFile.isEmpty()) { // it is still empty, create it qCWarning(MAILDIRRESOURCE_LOG) << "the resource is not properly configured: there is no config file for the resource. We create a new one."; const Collection::List cols = qobject_cast(job)->collections(); QString path; if (!cols.isEmpty()) { qCDebug(MAILDIRRESOURCE_LOG) << "the collections list is not empty"; Collection col = cols.first(); // get the path of the collection path = col.remoteId(); } // test the path if (path.isEmpty()) { qCDebug(MAILDIRRESOURCE_LOG) << "build a new path"; const QString dataDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/'); // we use "id" to get an unique path path = dataDir; if (!defaultResourceType().isEmpty()) { path += defaultResourceType() + QLatin1Char('/'); } path += id; qCDebug(MAILDIRRESOURCE_LOG) << "set the path" << path; mSettings->setPath(path); // set the resource into container mode for its top level mSettings->setTopLevelIsContainer(true); } else { // check how the directory looks like the actual check is missing. Maildir root(mSettings->path(), true); mSettings->setTopLevelIsContainer(root.isValid()); } qCDebug(MAILDIRRESOURCE_LOG) << "synchronize"; configurationChanged(); } } MaildirResource::~MaildirResource() { delete mSettings; } bool MaildirResource::retrieveItems(const Akonadi::Item::List &items, const QSet &parts) { Q_UNUSED(parts); const Maildir md = maildirForCollection(items.at(0).parentCollection()); if (!md.isValid()) { cancelTask(i18n("Unable to fetch item: The maildir folder \"%1\" is not valid.", md.path())); return false; } Akonadi::Item::List rv; rv.reserve(items.count()); for (const Akonadi::Item &item : items) { const QByteArray data = md.readEntry(item.remoteId()); KMime::Message *mail = new KMime::Message(); mail->setContent(KMime::CRLFtoLF(data)); mail->parse(); // Some messages may have an empty body if (mail->body().isEmpty()) { if (parts.contains("PLD:BODY") || parts.contains("PLD:RFC822")) { // In that case put a space in as body so that it gets cached // otherwise we'll wrongly believe the body part is missing from the cache mail->setBody(" "); } } Item i(item); i.setPayload(KMime::Message::Ptr(mail)); Akonadi::MessageFlags::copyMessageFlags(*mail, i); rv.push_back(i); } itemsRetrieved(rv); return true; } QString MaildirResource::itemMimeType() const { return KMime::Message::mimeType(); } void MaildirResource::configurationChanged() { mSettings->save(); bool configValid = ensureSaneConfiguration(); configValid = configValid && ensureDirExists(); if (configValid) { Q_EMIT status(Idle); setOnline(true); } if (name().isEmpty() || name() == identifier()) { Maildir md(mSettings->path()); setName(md.name()); } synchronizeCollectionTree(); } void MaildirResource::aboutToQuit() { // The settings may not have been saved if e.g. they have been modified via // DBus instead of the config dialog. mSettings->save(); } QString MaildirResource::defaultResourceType() { return QString(); } void MaildirResource::itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) { if (!ensureSaneConfiguration()) { cancelTask(i18n("Unusable configuration.")); return; } Maildir dir = maildirForCollection(collection); if (mSettings->readOnly() || !dir.isValid()) { cancelTask(dir.lastError()); return; } // we can only deal with mail if (!item.hasPayload()) { cancelTask(i18n("Error: Unsupported type.")); return; } const KMime::Message::Ptr mail = item.payload(); stopMaildirScan(dir); const QString rid = dir.addEntry(mail->encodedContent()); if (rid.isEmpty()) { restartMaildirScan(dir); cancelTask(dir.lastError()); return; } mChangedFiles.insert(rid); mChangedCleanerTimer->start(CLEANER_TIMEOUT); restartMaildirScan(dir); Item i(item); i.setRemoteId(rid); changeCommitted(i); } void MaildirResource::itemChanged(const Akonadi::Item &item, const QSet &parts) { if (!ensureSaneConfiguration()) { cancelTask(i18n("Unusable configuration.")); return; } bool bodyChanged = false; bool flagsChanged = false; bool headChanged = false; for (const QByteArray &part : parts) { if (part.startsWith("PLD:RFC822")) { bodyChanged = true; } else if (part.startsWith("PLD:HEAD")) { headChanged = true; } if (part.contains("FLAGS")) { flagsChanged = true; } } if (mSettings->readOnly() || (!bodyChanged && !flagsChanged && !headChanged)) { changeProcessed(); return; } Maildir dir = maildirForCollection(item.parentCollection()); if (!dir.isValid()) { cancelTask(dir.lastError()); return; } Item newItem(item); if (flagsChanged || bodyChanged || headChanged) { //something has changed that we can deal with stopMaildirScan(dir); if (flagsChanged) { //flags changed, store in file name and get back the new filename (id) const QString newKey = dir.changeEntryFlags(item.remoteId(), item.flags()); if (newKey.isEmpty()) { restartMaildirScan(dir); cancelTask(i18n("Failed to change the flags for the mail. %1", dir.lastError())); return; } newItem.setRemoteId(newKey); } if (bodyChanged || headChanged) { //head or body changed // we can only deal with mail if (item.hasPayload()) { const KMime::Message::Ptr mail = item.payload(); QByteArray data = mail->encodedContent(); if (headChanged && !bodyChanged) { //only the head has changed, get the current version of the mail //replace the head and store the new mail in the file const QByteArray currentData = dir.readEntry(newItem.remoteId()); if (currentData.isEmpty() && !dir.lastError().isEmpty()) { restartMaildirScan(dir); cancelTask(dir.lastError()); return; } const QByteArray newHead = mail->head(); mail->setContent(currentData); mail->setHead(newHead); mail->parse(); data = mail->encodedContent(); } if (!dir.writeEntry(newItem.remoteId(), data)) { restartMaildirScan(dir); cancelTask(dir.lastError()); return; } mChangedFiles.insert(newItem.remoteId()); mChangedCleanerTimer->start(CLEANER_TIMEOUT); } else { restartMaildirScan(dir); cancelTask(i18n("Maildir resource got a non-mail content.")); return; } } restartMaildirScan(dir); changeCommitted(newItem); } else { - Q_EMIT changeProcessed(); + changeProcessed(); } } void MaildirResource::itemMoved(const Item &item, const Collection &source, const Collection &destination) { if (source == destination) { // should not happen but would confuse Maildir::moveEntryTo changeProcessed(); return; } if (!ensureSaneConfiguration()) { cancelTask(i18n("Unusable configuration.")); return; } Maildir sourceDir = maildirForCollection(source); if (!sourceDir.isValid()) { cancelTask(i18n("Source folder is invalid: '%1'.", sourceDir.lastError())); return; } Maildir destDir = maildirForCollection(destination); if (!destDir.isValid()) { cancelTask(i18n("Destination folder is invalid: '%1'.", destDir.lastError())); return; } stopMaildirScan(sourceDir); stopMaildirScan(destDir); const QString newRid = sourceDir.moveEntryTo(item.remoteId(), destDir); mChangedFiles.insert(newRid); mChangedCleanerTimer->start(CLEANER_TIMEOUT); restartMaildirScan(sourceDir); restartMaildirScan(destDir); if (newRid.isEmpty()) { cancelTask(i18n("Could not move message '%1' from '%2' to '%3'. The error was %4.", item.remoteId(), sourceDir.path(), destDir.path(), sourceDir.lastError())); return; } Item i(item); i.setRemoteId(newRid); changeCommitted(i); } void MaildirResource::itemRemoved(const Akonadi::Item &item) { if (!ensureSaneConfiguration()) { cancelTask(i18n("Unusable configuration.")); return; } if (!mSettings->readOnly()) { Maildir dir = maildirForCollection(item.parentCollection()); // !dir.isValid() means that our parent folder has been deleted already, // so we don't care at all as that one will be recursive anyway stopMaildirScan(dir); if (dir.isValid() && !dir.removeEntry(item.remoteId())) { Q_EMIT error(i18n("Failed to delete message: %1", item.remoteId())); } restartMaildirScan(dir); } qCDebug(MAILDIRRESOURCE_LOG) << "Item removed" << item.id() << " in collection :" << item.parentCollection().id(); changeProcessed(); } Collection::List MaildirResource::listRecursive(const Collection &root, const Maildir &dir) { if (mSettings->monitorFilesystem()) { mFsWatcher->addDir(dir.path() + QDir::separator() + QLatin1String("new")); mFsWatcher->addDir(dir.path() + QDir::separator() + QLatin1String("cur")); mFsWatcher->addDir(dir.subDirPath()); if (dir.isRoot()) { mFsWatcher->addDir(dir.path()); } } Collection::List list; const QStringList mimeTypes = QStringList() << itemMimeType() << Collection::mimeType(); const QStringList lstDir = dir.subFolderList(); for (const QString &sub : lstDir) { Collection c; c.setName(sub); c.setRemoteId(sub); c.setParentCollection(root); c.setContentMimeTypes(mimeTypes); if (sub.compare(QLatin1String("inbox"), Qt::CaseInsensitive) == 0) { c.attribute(Collection::AddIfMissing)->setCollectionType("inbox"); } const Maildir md = maildirForCollection(c); if (!md.isValid()) { continue; } list << c; list += listRecursive(c, md); } return list; } void MaildirResource::retrieveCollections() { Maildir dir(mSettings->path(), mSettings->topLevelIsContainer()); if (!dir.isValid()) { Q_EMIT error(dir.lastError()); collectionsRetrieved(Collection::List()); return; } Collection root; root.setParentCollection(Collection::root()); root.setRemoteId(mSettings->path()); root.setName(name()); if (mSettings->readOnly()) { root.setRights(Collection::ReadOnly); } else { if (mSettings->topLevelIsContainer()) { root.setRights(Collection::ReadOnly | Collection::CanCreateCollection); } else { root.setRights(Collection::CanChangeItem | Collection::CanCreateItem | Collection::CanDeleteItem | Collection::CanCreateCollection); } } CachePolicy policy; policy.setInheritFromParent(false); policy.setSyncOnDemand(true); policy.setLocalParts(QStringList() << QLatin1String(MessagePart::Envelope)); policy.setCacheTimeout(1); policy.setIntervalCheckTime(-1); root.setCachePolicy(policy); QStringList mimeTypes; mimeTypes << Collection::mimeType(); mimeTypes << itemMimeType(); root.setContentMimeTypes(mimeTypes); Collection::List list; list << root; list += listRecursive(root, dir); collectionsRetrieved(list); } void MaildirResource::retrieveItems(const Akonadi::Collection &col) { const Maildir md = maildirForCollection(col); if (!md.isValid()) { cancelTask(i18n("Maildir '%1' for collection '%2' is invalid.", md.path(), col.remoteId())); return; } RetrieveItemsJob *job = new RetrieveItemsJob(col, md, this); job->setMimeType(itemMimeType()); connect(job, &RetrieveItemsJob::result, this, &MaildirResource::slotItemsRetrievalResult); } void MaildirResource::slotItemsRetrievalResult(KJob *job) { if (job->error()) { cancelTask(job->errorString()); } else { itemsRetrievalDone(); } } void MaildirResource::collectionAdded(const Collection &collection, const Collection &parent) { if (!ensureSaneConfiguration()) { Q_EMIT error(i18n("Unusable configuration.")); changeProcessed(); return; } Maildir md = maildirForCollection(parent); qCDebug(MAILDIRRESOURCE_LOG) << md.subFolderList(); if (mSettings->readOnly() || !md.isValid()) { changeProcessed(); return; } else { const QString collectionName(collection.name().remove(QDir::separator())); const QString newFolderPath = md.addSubFolder(collectionName); if (newFolderPath.isEmpty()) { changeProcessed(); return; } qCDebug(MAILDIRRESOURCE_LOG) << md.subFolderList(); Collection col = collection; col.setRemoteId(collectionName); col.setName(collectionName); changeCommitted(col); } } void MaildirResource::collectionChanged(const Collection &collection) { if (!ensureSaneConfiguration()) { Q_EMIT error(i18n("Unusable configuration.")); changeProcessed(); return; } if (collection.parentCollection() == Collection::root()) { if (collection.name() != name()) { setName(collection.name()); } changeProcessed(); return; } if (collection.remoteId() == collection.name()) { changeProcessed(); return; } Maildir md = maildirForCollection(collection); if (!md.isValid()) { assert(!collection.remoteId().isEmpty()); // caught in resourcebase // we don't have a maildir for this collection yet, probably due to a race // make one, otherwise the rename below will fail md.create(); } const QString collectionName(collection.name().remove(QDir::separator())); if (!md.rename(collectionName)) { Q_EMIT error(i18n("Unable to rename maildir folder '%1'.", collection.name())); changeProcessed(); return; } Collection c(collection); c.setRemoteId(collectionName); c.setName(collectionName); changeCommitted(c); } void MaildirResource::collectionMoved(const Collection &collection, const Collection &source, const Collection &dest) { qCDebug(MAILDIRRESOURCE_LOG) << collection << source << dest; if (!ensureSaneConfiguration()) { Q_EMIT error(i18n("Unusable configuration.")); changeProcessed(); return; } if (collection.parentCollection() == Collection::root()) { Q_EMIT error(i18n("Cannot move root maildir folder '%1'.", collection.remoteId())); changeProcessed(); return; } if (source == dest) { // should not happen, but who knows... changeProcessed(); return; } Collection c(collection); c.setParentCollection(source); Maildir md = maildirForCollection(c); Maildir destMd = maildirForCollection(dest); if (!md.moveTo(destMd)) { Q_EMIT error(i18n("Unable to move maildir folder '%1' from '%2' to '%3'.", collection.remoteId(), source.remoteId(), dest.remoteId())); changeProcessed(); } else { const QString path = maildirPathForCollection(c); mMaildirsForCollection.remove(path); changeCommitted(collection); } } void MaildirResource::collectionRemoved(const Akonadi::Collection &collection) { if (!ensureSaneConfiguration()) { Q_EMIT error(i18n("Unusable configuration.")); changeProcessed(); return; } if (collection.parentCollection() == Collection::root()) { Q_EMIT error(i18n("Cannot delete top-level maildir folder '%1'.", mSettings->path())); changeProcessed(); return; } Maildir md = maildirForCollection(collection.parentCollection()); // !md.isValid() means that our parent folder has been deleted already, // so we don't care at all as that one will be recursive anyway if (md.isValid() && !md.removeSubFolder(collection.remoteId())) { Q_EMIT error(i18n("Failed to delete sub-folder '%1'.", collection.remoteId())); } const QString path = maildirPathForCollection(collection); mMaildirsForCollection.remove(path); changeProcessed(); } bool MaildirResource::ensureDirExists() { Maildir root(mSettings->path()); if (!root.isValid(false) && !mSettings->topLevelIsContainer()) { if (!root.create()) { Q_EMIT status(Broken, i18n("Unable to create maildir '%1'.", mSettings->path())); } return false; } return true; } bool MaildirResource::ensureSaneConfiguration() { if (mSettings->path().isEmpty()) { Q_EMIT status(NotConfigured, i18n("No usable storage location configured.")); setOnline(false); return false; } return true; } void MaildirResource::slotDirChanged(const QString &dir) { QFileInfo fileInfo(dir); if (fileInfo.isFile()) { slotFileChanged(fileInfo); return; } if (dir == mSettings->path()) { synchronizeCollectionTree(); synchronizeCollection(Collection::root().id()); return; } if (dir.endsWith(QLatin1String(".directory"))) { synchronizeCollectionTree(); //might be too much, but this is not a common case anyway return; } QDir d(dir); if (!d.cdUp()) { return; } Maildir md(d.path()); if (!md.isValid()) { return; } md.refreshKeyCache(); const Collection col = collectionForMaildir(md); if (col.remoteId().isEmpty()) { qCDebug(MAILDIRRESOURCE_LOG) << "unable to find collection for path" << dir; return; } CollectionFetchJob *job = new CollectionFetchJob(col, Akonadi::CollectionFetchJob::Base, this); connect(job, &CollectionFetchJob::result, this, &MaildirResource::fsWatchDirFetchResult); } void MaildirResource::fsWatchDirFetchResult(KJob *job) { if (job->error()) { qCDebug(MAILDIRRESOURCE_LOG) << job->errorString(); return; } const Collection::List cols = qobject_cast(job)->collections(); if (cols.isEmpty()) { return; } synchronizeCollection(cols.first().id()); } void MaildirResource::slotFileChanged(const QFileInfo &fileInfo) { const QString key = fileInfo.fileName(); if (mChangedFiles.remove(key) > 0) { return; } QString path = fileInfo.path(); if (path.endsWith(QLatin1String("/new"))) { path.remove(path.length() - 4, 4); } else if (path.endsWith(QLatin1String("/cur"))) { path.remove(path.length() - 4, 4); } const Maildir md(path); if (!md.isValid()) { return; } const Collection col = collectionForMaildir(md); if (col.remoteId().isEmpty()) { qCDebug(MAILDIRRESOURCE_LOG) << "unable to find collection for path" << fileInfo.path(); return; } Item item; item.setRemoteId(key); item.setParentCollection(col); ItemFetchJob *job = new ItemFetchJob(item, this); job->setProperty("entry", key); job->setProperty("dir", path); connect(job, &ItemFetchJob::result, this, &MaildirResource::fsWatchFileFetchResult); } void MaildirResource::fsWatchFileFetchResult(KJob *job) { if (job->error()) { qCDebug(MAILDIRRESOURCE_LOG) << job->errorString(); return; } Item::List items = qobject_cast(job)->items(); if (items.isEmpty()) { return; } const QString fileName = job->property("entry").toString(); const QString path = job->property("dir").toString(); const Maildir md(path); Item item(items.at(0)); const qint64 entrySize = md.size(fileName); if (entrySize >= 0) { item.setSize(entrySize); } const Item::Flags flags = md.readEntryFlags(fileName); for (const Item::Flag &flag : flags) { item.setFlag(flag); } const QByteArray data = md.readEntry(fileName); KMime::Message *mail = new KMime::Message(); mail->setContent(KMime::CRLFtoLF(data)); mail->parse(); item.setPayload(KMime::Message::Ptr(mail)); Akonadi::MessageFlags::copyMessageFlags(*mail, item); ItemModifyJob *mjob = new ItemModifyJob(item); connect(mjob, &ItemModifyJob::result, this, &MaildirResource::fsWatchFileModifyResult); } void MaildirResource::fsWatchFileModifyResult(KJob *job) { if (job->error()) { qCDebug(MAILDIRRESOURCE_LOG) << " MaildirResource::fsWatchFileModifyResult error: " << job->errorString(); return; } } QString MaildirResource::maildirPathForCollection(const Collection &collection) const { QString path = collection.remoteId(); Akonadi::Collection parent = collection.parentCollection(); while (!parent.remoteId().isEmpty()) { path.prepend(parent.remoteId() + QLatin1Char('/')); parent = parent.parentCollection(); } return path; } void MaildirResource::stopMaildirScan(const Maildir &maildir) { const QString path = maildir.path(); mFsWatcher->removeDir(path + QLatin1String("/new")); mFsWatcher->removeDir(path + QLatin1String("/cur")); } void MaildirResource::restartMaildirScan(const Maildir &maildir) { const QString path = maildir.path(); mFsWatcher->addDir(path + QLatin1String("/new")); mFsWatcher->addDir(path + QLatin1String("/cur")); } void MaildirResource::changedCleaner() { mChangedFiles.clear(); } diff --git a/resources/mixedmaildir/mixedmaildirstore.cpp b/resources/mixedmaildir/mixedmaildirstore.cpp index 5194dac60..3f824f8b2 100644 --- a/resources/mixedmaildir/mixedmaildirstore.cpp +++ b/resources/mixedmaildir/mixedmaildirstore.cpp @@ -1,2420 +1,2421 @@ /* This file is part of the KDE project Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net Author: Kevin Krammer, krake@kdab.com 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 "mixedmaildirstore.h" #include "kmindexreader/kmindexreader.h" #include "mixedmaildir_debug.h" #include "filestore/collectioncreatejob.h" #include "filestore/collectiondeletejob.h" #include "filestore/collectionfetchjob.h" #include "filestore/collectionmovejob.h" #include "filestore/collectionmodifyjob.h" #include "filestore/entitycompactchangeattribute.h" #include "filestore/itemcreatejob.h" #include "filestore/itemdeletejob.h" #include "filestore/itemfetchjob.h" #include "filestore/itemmodifyjob.h" #include "filestore/itemmovejob.h" #include "filestore/storecompactjob.h" #include "libmaildir/maildir.h" #include #include #include #include #include #include #include #include #include "mixedmaildirresource_debug.h" #include #include #include using namespace Akonadi; using KPIM::Maildir; using namespace KMBox; static bool fullEntryCompare(const KMBox::MBoxEntry &left, const KMBox::MBoxEntry &right) { return left.messageOffset() == right.messageOffset() && left.separatorSize() == right.separatorSize() && left.messageSize() == right.messageSize(); } static bool indexFileForFolder(const QFileInfo &folderDirInfo, QFileInfo &indexFileInfo) { indexFileInfo = QFileInfo(folderDirInfo.dir(), QStringLiteral(".%1.index").arg(folderDirInfo.fileName())); if (!indexFileInfo.exists() || !indexFileInfo.isReadable()) { qCDebug(MIXEDMAILDIR_LOG) << "No index file" << indexFileInfo.absoluteFilePath() << "or not readable"; return false; } return true; } class MBoxContext { public: MBoxContext() : mRevision(0) , mIndexDataLoaded(false) , mHasIndexData(false) { } QString fileName() const { return mMBox.fileName(); } bool load(const QString &fileName) { mModificationTime = QFileInfo(fileName).lastModified(); // in case of reload, check if anything changed, otherwise keep deleted entries if (!mDeletedOffsets.isEmpty() && fileName == mMBox.fileName()) { const KMBox::MBoxEntry::List currentEntryList = mMBox.entries(); if (mMBox.load(fileName)) { const KMBox::MBoxEntry::List newEntryList = mMBox.entries(); if (!std::equal(currentEntryList.begin(), currentEntryList.end(), newEntryList.begin(), fullEntryCompare)) { mDeletedOffsets.clear(); } return true; } return false; } mDeletedOffsets.clear(); return mMBox.load(fileName); } QDateTime modificationTime() const { return mModificationTime; } KMBox::MBoxEntry::List entryList() const { KMBox::MBoxEntry::List result; result.reserve(mMBox.entries().count()); Q_FOREACH (const KMBox::MBoxEntry &entry, mMBox.entries()) { if (!mDeletedOffsets.contains(entry.messageOffset())) { result << entry; } } return result; } QByteArray readRawEntry(quint64 offset) { return mMBox.readRawMessage(KMBox::MBoxEntry(offset)); } QByteArray readEntryHeaders(quint64 offset) { return mMBox.readMessageHeaders(KMBox::MBoxEntry(offset)); } qint64 appendEntry(const KMime::Message::Ptr &entry) { const KMBox::MBoxEntry result = mMBox.appendMessage(entry); if (result.isValid() && mHasIndexData) { mIndexData.insert(result.messageOffset(), KMIndexDataPtr(new KMIndexData)); Q_ASSERT(mIndexData.value(result.messageOffset())->isEmpty()); } return result.messageOffset(); } void deleteEntry(quint64 offset) { mDeletedOffsets << offset; } bool isValidOffset(quint64 offset) const { if (mDeletedOffsets.contains(offset)) { return false; } const KMBox::MBoxEntry::List lstEntry = mMBox.entries(); for (const KMBox::MBoxEntry &entry : lstEntry) { if (entry.messageOffset() == offset) { return true; } } return false; } bool save() { bool ret = mMBox.save(); mModificationTime = QDateTime::currentDateTime(); return ret; } int purge(QList &movedEntries) { const int deleteCount = mDeletedOffsets.count(); KMBox::MBoxEntry::List deletedEntries; deletedEntries.reserve(deleteCount); Q_FOREACH (quint64 offset, mDeletedOffsets) { deletedEntries << KMBox::MBoxEntry(offset); } const bool result = mMBox.purge(deletedEntries, &movedEntries); if (mHasIndexData) { // keep copy of original for lookup const IndexDataHash indexData = mIndexData; // delete index data for removed entries Q_FOREACH (quint64 offset, mDeletedOffsets) { mIndexData.remove(offset); } // delete index data for changed entries // readded below in an extra loop to handled cases where a new index is equal to an // old index of a different entry Q_FOREACH (const KMBox::MBoxEntry::Pair &entry, movedEntries) { mIndexData.remove(entry.first.messageOffset()); } // readd index data for changed entries at their new position Q_FOREACH (const KMBox::MBoxEntry::Pair &entry, movedEntries) { const KMIndexDataPtr data = indexData.value(entry.first.messageOffset()); mIndexData.insert(entry.second.messageOffset(), data); } } mDeletedOffsets.clear(); mModificationTime = QDateTime::currentDateTime(); return result ? deleteCount : -1; } MBox &mbox() { return mMBox; } const MBox &mbox() const { return mMBox; } bool hasDeletedOffsets() const { return !mDeletedOffsets.isEmpty(); } void readIndexData(); KMIndexDataPtr indexData(quint64 offset) const { if (mHasIndexData) { if (!mDeletedOffsets.contains(offset)) { IndexDataHash::const_iterator it = mIndexData.constFind(offset); if (it != mIndexData.constEnd()) { return it.value(); } } } return KMIndexDataPtr(); } bool hasIndexData() const { return mHasIndexData; } void updatePath(const QString &newPath) { // TODO FIXME there has to be a better of doing that mMBox.load(newPath); } public: Collection mCollection; qint64 mRevision; private: QSet mDeletedOffsets; MBox mMBox; QDateTime mModificationTime; typedef QHash IndexDataHash; IndexDataHash mIndexData; bool mIndexDataLoaded; bool mHasIndexData; }; typedef QSharedPointer MBoxPtr; void MBoxContext::readIndexData() { if (mIndexDataLoaded) { return; } mIndexDataLoaded = true; const QFileInfo mboxFileInfo(mMBox.fileName()); QFileInfo indexFileInfo; if (!indexFileForFolder(mboxFileInfo, indexFileInfo)) { return; } if (mboxFileInfo.lastModified() > indexFileInfo.lastModified()) { qCDebug(MIXEDMAILDIR_LOG) << "MBox file " << mboxFileInfo.absoluteFilePath() << "newer than the index: mbox modified at" << mboxFileInfo.lastModified() << ", index modified at" << indexFileInfo.lastModified(); return; } KMIndexReader indexReader(indexFileInfo.absoluteFilePath()); if (indexReader.error() || !indexReader.readIndex()) { qCCritical(MIXEDMAILDIRRESOURCE_LOG) << "Index file" << indexFileInfo.path() << "could not be read"; return; } mHasIndexData = true; const KMBox::MBoxEntry::List entries = mMBox.entries(); for (const KMBox::MBoxEntry &entry : entries) { const quint64 indexOffset = entry.messageOffset() + entry.separatorSize(); const KMIndexDataPtr data = indexReader.dataByOffset(indexOffset); if (data != nullptr) { mIndexData.insert(entry.messageOffset(), data); } } qCDebug(MIXEDMAILDIR_LOG) << "Read" << mIndexData.count() << "index entries from" << indexFileInfo.absoluteFilePath(); } class MaildirContext { public: MaildirContext(const QString &path, bool isTopLevel) : mMaildir(path, isTopLevel) , mIndexDataLoaded(false) , mHasIndexData(false) { } MaildirContext(const Maildir &md) : mMaildir(md) , mIndexDataLoaded(false) , mHasIndexData(false) { } QStringList entryList() const { return mMaildir.entryList(); } QString addEntry(const QByteArray &data) { const QString result = mMaildir.addEntry(data); if (!result.isEmpty() && mHasIndexData) { mIndexData.insert(result, KMIndexDataPtr(new KMIndexData)); Q_ASSERT(mIndexData.value(result)->isEmpty()); } else { //TODO: use the error string? qCWarning(MIXEDMAILDIRRESOURCE_LOG) << mMaildir.lastError(); } return result; } void writeEntry(const QString &key, const QByteArray &data) { mMaildir.writeEntry(key, data); //TODO: error handling if (mHasIndexData) { mIndexData.insert(key, KMIndexDataPtr(new KMIndexData)); } } bool removeEntry(const QString &key) { const bool result = mMaildir.removeEntry(key); if (result && mHasIndexData) { mIndexData.remove(key); } return result; } QString moveEntryTo(const QString &key, MaildirContext &destination) { const QString result = mMaildir.moveEntryTo(key, destination.mMaildir); if (!result.isEmpty()) { if (mHasIndexData) { mIndexData.remove(key); } if (destination.mHasIndexData) { destination.mIndexData.insert(result, KMIndexDataPtr(new KMIndexData)); } } else { //TODO error handling? qCWarning(MIXEDMAILDIRRESOURCE_LOG) << mMaildir.lastError(); } return result; } QByteArray readEntryHeaders(const QString &key) const { return mMaildir.readEntryHeaders(key); } QByteArray readEntry(const QString &key) const { return mMaildir.readEntry(key); } bool isValid(QString &error) const { bool result = mMaildir.isValid(); if (!result) { error = mMaildir.lastError(); } return result; } bool isValidEntry(const QString &entry) const { return !mMaildir.findRealKey(entry).isEmpty(); } void readIndexData(); KMIndexDataPtr indexData(const QString &fileName) const { if (mHasIndexData) { IndexDataHash::const_iterator it = mIndexData.constFind(fileName); if (it != mIndexData.constEnd()) { return it.value(); } } return KMIndexDataPtr(); } bool hasIndexData() const { return mHasIndexData; } void updatePath(const QString &newPath) { mMaildir = Maildir(newPath, mMaildir.isRoot()); } const Maildir &maildir() const { return mMaildir; } private: Maildir mMaildir; typedef QHash IndexDataHash; IndexDataHash mIndexData; bool mIndexDataLoaded; bool mHasIndexData; }; void MaildirContext::readIndexData() { if (mIndexDataLoaded) { return; } mIndexDataLoaded = true; const QFileInfo maildirFileInfo(mMaildir.path()); QFileInfo indexFileInfo; if (!indexFileForFolder(maildirFileInfo, indexFileInfo)) { return; } const QDir maildirBaseDir(maildirFileInfo.absoluteFilePath()); const QFileInfo curDirFileInfo(maildirBaseDir, QStringLiteral("cur")); const QFileInfo newDirFileInfo(maildirBaseDir, QStringLiteral("new")); if (curDirFileInfo.lastModified() > indexFileInfo.lastModified()) { qCDebug(MIXEDMAILDIR_LOG) << "Maildir " << maildirFileInfo.absoluteFilePath() << "\"cur\" directory newer than the index: cur modified at" << curDirFileInfo.lastModified() << ", index modified at" << indexFileInfo.lastModified(); return; } if (newDirFileInfo.lastModified() > indexFileInfo.lastModified()) { qCDebug(MIXEDMAILDIR_LOG) << "Maildir \"new\" directory newer than the index: cur modified at" << newDirFileInfo.lastModified() << ", index modified at" << indexFileInfo.lastModified(); return; } KMIndexReader indexReader(indexFileInfo.absoluteFilePath()); if (indexReader.error() || !indexReader.readIndex()) { qCCritical(MIXEDMAILDIRRESOURCE_LOG) << "Index file" << indexFileInfo.path() << "could not be read"; return; } mHasIndexData = true; const QStringList entries = mMaildir.entryList(); for (const QString &entry : entries) { const KMIndexDataPtr data = indexReader.dataByFileName(entry); if (data != nullptr) { mIndexData.insert(entry, data); } } qCDebug(MIXEDMAILDIR_LOG) << "Read" << mIndexData.count() << "index entries from" << indexFileInfo.absoluteFilePath(); } typedef QSharedPointer MaildirPtr; class MixedMaildirStore::Private : public FileStore::Job::Visitor { MixedMaildirStore *const q; public: enum FolderType { InvalidFolder, TopLevelFolder, MaildirFolder, MBoxFolder }; Private(MixedMaildirStore *parent) : q(parent) { } FolderType folderForCollection(const Collection &col, QString &path, QString &errorText) const; MBoxPtr getOrCreateMBoxPtr(const QString &path); MaildirPtr getOrCreateMaildirPtr(const QString &path, bool isTopLevel); void fillMBoxCollectionDetails(const MBoxPtr &mbox, Collection &collection); void fillMaildirCollectionDetails(const Maildir &md, Collection &collection); void fillMaildirTreeDetails(const Maildir &md, const Collection &collection, Collection::List &collections, bool recurse); void listCollection(FileStore::Job *job, MBoxPtr &mbox, const Collection &collection, Item::List &items); void listCollection(FileStore::Job *job, MaildirPtr &md, const Collection &collection, Item::List &items); bool fillItem(MBoxPtr &mbox, bool includeHeaders, bool includeBody, Item &item) const; bool fillItem(const MaildirPtr &md, bool includeHeaders, bool includeBody, Item &item) const; void updateContextHashes(const QString &oldPath, const QString &newPath); public: // visitor interface implementation bool visit(FileStore::Job *job) override; bool visit(FileStore::CollectionCreateJob *job) override; bool visit(FileStore::CollectionDeleteJob *job) override; bool visit(FileStore::CollectionFetchJob *job) override; bool visit(FileStore::CollectionModifyJob *job) override; bool visit(FileStore::CollectionMoveJob *job) override; bool visit(FileStore::ItemCreateJob *job) override; bool visit(FileStore::ItemDeleteJob *job) override; bool visit(FileStore::ItemFetchJob *job) override; bool visit(FileStore::ItemModifyJob *job) override; bool visit(FileStore::ItemMoveJob *job) override; bool visit(FileStore::StoreCompactJob *job) override; public: typedef QHash MBoxHash; MBoxHash mMBoxes; typedef QHash MaildirHash; MaildirHash mMaildirs; }; MixedMaildirStore::Private::FolderType MixedMaildirStore::Private::folderForCollection(const Collection &col, QString &path, QString &errorText) const { path.clear(); errorText.clear(); if (col.remoteId().isEmpty()) { errorText = i18nc("@info:status", "Given folder name is empty"); qCWarning(MIXEDMAILDIRRESOURCE_LOG) << "Incomplete ancestor chain for collection."; Q_ASSERT(!col.remoteId().isEmpty()); // abort! Look at backtrace to see where we came from. return InvalidFolder; } if (col.parentCollection() == Collection::root()) { path = q->path(); if (col.remoteId() != path) { qCWarning(MIXEDMAILDIRRESOURCE_LOG) << "RID mismatch, is" << col.remoteId() << "expected" << path; } return TopLevelFolder; } FolderType type = folderForCollection(col.parentCollection(), path, errorText); switch (type) { case InvalidFolder: return InvalidFolder; case TopLevelFolder: // fall through case MaildirFolder: { const Maildir parentMd(path, type == TopLevelFolder); const Maildir subFolder = parentMd.subFolder(col.remoteId()); if (subFolder.isValid(false)) { path = subFolder.path(); return MaildirFolder; } const QString subDirPath = (type == TopLevelFolder ? path : Maildir::subDirPathForFolderPath(path)); QFileInfo fileInfo(QDir(subDirPath), col.remoteId()); if (fileInfo.isFile()) { path = fileInfo.absoluteFilePath(); return MBoxFolder; } errorText = i18nc("@info:status", "Folder %1 does not seem to be a valid email folder", fileInfo.absoluteFilePath()); return InvalidFolder; } case MBoxFolder: { const QString subDirPath = Maildir::subDirPathForFolderPath(path); QFileInfo fileInfo(QDir(subDirPath), col.remoteId()); if (fileInfo.isFile()) { path = fileInfo.absoluteFilePath(); return MBoxFolder; } const Maildir subFolder(fileInfo.absoluteFilePath(), false); if (subFolder.isValid(false)) { path = subFolder.path(); return MaildirFolder; } errorText = i18nc("@info:status", "Folder %1 does not seem to be a valid email folder", fileInfo.absoluteFilePath()); return InvalidFolder; } } return InvalidFolder; } MBoxPtr MixedMaildirStore::Private::getOrCreateMBoxPtr(const QString &path) { MBoxPtr mbox; const MBoxHash::const_iterator it = mMBoxes.constFind(path); if (it == mMBoxes.constEnd()) { mbox = MBoxPtr(new MBoxContext); mMBoxes.insert(path, mbox); } else { mbox = it.value(); } return mbox; } MaildirPtr MixedMaildirStore::Private::getOrCreateMaildirPtr(const QString &path, bool isTopLevel) { MaildirPtr md; const MaildirHash::const_iterator it = mMaildirs.constFind(path); if (it == mMaildirs.constEnd()) { md = MaildirPtr(new MaildirContext(path, isTopLevel)); mMaildirs.insert(path, md); } else { md = it.value(); } return md; } void MixedMaildirStore::Private::fillMBoxCollectionDetails(const MBoxPtr &mbox, Collection &collection) { collection.setContentMimeTypes(QStringList() << Collection::mimeType() << KMime::Message::mimeType()); if (collection.name().compare(QLatin1String("inbox"), Qt::CaseInsensitive) == 0) { collection.attribute(Collection::AddIfMissing)->setCollectionType("inbox"); } const QFileInfo fileInfo(mbox->fileName()); if (fileInfo.isWritable()) { collection.setRights(Collection::CanCreateItem |Collection::CanChangeItem |Collection::CanDeleteItem |Collection::CanCreateCollection |Collection::CanChangeCollection |Collection::CanDeleteCollection); } else { collection.setRights(Collection::ReadOnly); } if (mbox->mRevision > 0) { collection.setRemoteRevision(QString::number(mbox->mRevision)); } } void MixedMaildirStore::Private::fillMaildirCollectionDetails(const Maildir &md, Collection &collection) { collection.setContentMimeTypes(QStringList() << Collection::mimeType() << KMime::Message::mimeType()); if (collection.name().compare(QLatin1String("inbox"), Qt::CaseInsensitive) == 0) { collection.attribute(Collection::AddIfMissing)->setCollectionType("inbox"); } const QFileInfo fileInfo(md.path()); if (fileInfo.isWritable()) { collection.setRights(Collection::CanCreateItem |Collection::CanChangeItem |Collection::CanDeleteItem |Collection::CanCreateCollection |Collection::CanChangeCollection |Collection::CanDeleteCollection); } else { collection.setRights(Collection::ReadOnly); } } void MixedMaildirStore::Private::fillMaildirTreeDetails(const Maildir &md, const Collection &collection, Collection::List &collections, bool recurse) { if (md.path().isEmpty()) { return; } const QStringList maildirSubFolders = md.subFolderList(); for (const QString &subFolder : maildirSubFolders) { const Maildir subMd = md.subFolder(subFolder); if (!mMaildirs.contains(subMd.path())) { const MaildirPtr mdPtr = MaildirPtr(new MaildirContext(subMd)); mMaildirs.insert(subMd.path(), mdPtr); } Collection col; col.setRemoteId(subFolder); col.setName(subFolder); col.setParentCollection(collection); fillMaildirCollectionDetails(subMd, col); collections << col; if (recurse) { fillMaildirTreeDetails(subMd, col, collections, true); } } const QDir dir(md.isRoot() ? md.path() : Maildir::subDirPathForFolderPath(md.path())); const QFileInfoList fileInfos = dir.entryInfoList(QDir::Files); for (const QFileInfo &fileInfo : fileInfos) { if (fileInfo.isHidden() || !fileInfo.isReadable()) { continue; } const QString mboxPath = fileInfo.absoluteFilePath(); MBoxPtr mbox = getOrCreateMBoxPtr(mboxPath); if (mbox->load(mboxPath)) { const QString subFolder = fileInfo.fileName(); Collection col; col.setRemoteId(subFolder); col.setName(subFolder); col.setParentCollection(collection); mbox->mCollection = col; fillMBoxCollectionDetails(mbox, col); collections << col; if (recurse) { const QString subDirPath = Maildir::subDirPathForFolderPath(fileInfo.absoluteFilePath()); const Maildir subMd(subDirPath, true); fillMaildirTreeDetails(subMd, col, collections, true); } } else { mMBoxes.remove(fileInfo.absoluteFilePath()); } } } void MixedMaildirStore::Private::listCollection(FileStore::Job *job, MBoxPtr &mbox, const Collection &collection, Item::List &items) { mbox->readIndexData(); QHash uidHash; QHash tagListHash; const KMBox::MBoxEntry::List entryList = mbox->entryList(); for (const KMBox::MBoxEntry &entry : entryList) { Item item; item.setMimeType(KMime::Message::mimeType()); item.setRemoteId(QString::number(entry.messageOffset())); item.setParentCollection(collection); if (mbox->hasIndexData()) { const KMIndexDataPtr indexData = mbox->indexData(entry.messageOffset()); if (indexData != nullptr && !indexData->isEmpty()) { item.setFlags(indexData->status().statusFlags()); quint64 uid = indexData->uid(); if (uid != 0) { qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "item" << item.remoteId() << "has UID" << uid; uidHash.insert(item.remoteId(), QString::number(uid)); } const QStringList tagList = indexData->tagList(); if (!tagList.isEmpty()) { qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "item" << item.remoteId() << "has" << tagList.count() << "tags:" << tagList; tagListHash.insert(item.remoteId(), tagList); } } else if (indexData == nullptr) { Akonadi::MessageStatus status; status.setDeleted(true); item.setFlags(status.statusFlags()); qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "no index for item" << item.remoteId() << "in MBox" << mbox->fileName() << "so it has been deleted but not purged. Marking it as" << item.flags(); } } items << item; } if (mbox->hasIndexData()) { QVariant var; if (!uidHash.isEmpty()) { var = QVariant::fromValue< QHash >(uidHash); job->setProperty("remoteIdToIndexUid", var); } if (!tagListHash.isEmpty()) { var = QVariant::fromValue< QHash >(tagListHash); job->setProperty("remoteIdToTagList", var); } } } void MixedMaildirStore::Private::listCollection(FileStore::Job *job, MaildirPtr &md, const Collection &collection, Item::List &items) { md->readIndexData(); QHash uidHash; QHash tagListHash; const QStringList entryList = md->entryList(); for (const QString &entry : entryList) { Item item; item.setMimeType(KMime::Message::mimeType()); item.setRemoteId(entry); item.setParentCollection(collection); if (md->hasIndexData()) { const KMIndexDataPtr indexData = md->indexData(entry); if (indexData != nullptr && !indexData->isEmpty()) { item.setFlags(indexData->status().statusFlags()); const quint64 uid = indexData->uid(); if (uid != 0) { qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "item" << item.remoteId() << "has UID" << uid; uidHash.insert(item.remoteId(), QString::number(uid)); } const QStringList tagList = indexData->tagList(); if (!tagList.isEmpty()) { qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "item" << item.remoteId() << "has" << tagList.count() << "tags:" << tagList; tagListHash.insert(item.remoteId(), tagList); } } } const Akonadi::Item::Flags flags = md->maildir().readEntryFlags(entry); for (const Akonadi::Item::Flag &flag : flags) { item.setFlag(flag); } items << item; } if (md->hasIndexData()) { QVariant var; if (!uidHash.isEmpty()) { var = QVariant::fromValue< QHash >(uidHash); job->setProperty("remoteIdToIndexUid", var); } if (!tagListHash.isEmpty()) { var = QVariant::fromValue< QHash >(tagListHash); job->setProperty("remoteIdToTagList", var); } } } bool MixedMaildirStore::Private::fillItem(MBoxPtr &mbox, bool includeHeaders, bool includeBody, Item &item) const { // qCDebug(MIXEDMAILDIR_LOG) << "Filling item" << item.remoteId() << "from MBox: includeBody=" << includeBody; bool ok = false; const quint64 offset = item.remoteId().toULongLong(&ok); if (!ok || !mbox->isValidOffset(offset)) { return false; } item.setModificationTime(mbox->modificationTime()); // TODO: size? if (includeHeaders || includeBody) { KMime::Message::Ptr messagePtr(new KMime::Message()); if (includeBody) { const QByteArray data = mbox->readRawEntry(offset); messagePtr->setContent(KMime::CRLFtoLF(data)); } else { const QByteArray data = mbox->readEntryHeaders(offset); messagePtr->setHead(KMime::CRLFtoLF(data)); } messagePtr->parse(); item.setPayload(messagePtr); Akonadi::MessageFlags::copyMessageFlags(*messagePtr, item); } return true; } bool MixedMaildirStore::Private::fillItem(const MaildirPtr &md, bool includeHeaders, bool includeBody, Item &item) const { /* qCDebug(MIXEDMAILDIR_LOG) << "Filling item" << item.remoteId() << "from Maildir: includeBody=" << includeBody;*/ const qint64 entrySize = md->maildir().size(item.remoteId()); if (entrySize < 0) { return false; } item.setSize(entrySize); item.setModificationTime(md->maildir().lastModified(item.remoteId())); if (includeHeaders || includeBody) { KMime::Message::Ptr messagePtr(new KMime::Message()); if (includeBody) { const QByteArray data = md->readEntry(item.remoteId()); if (data.isEmpty()) { return false; } messagePtr->setContent(KMime::CRLFtoLF(data)); } else { const QByteArray data = md->readEntryHeaders(item.remoteId()); if (data.isEmpty()) { return false; } messagePtr->setHead(KMime::CRLFtoLF(data)); } messagePtr->parse(); item.setPayload(messagePtr); Akonadi::MessageFlags::copyMessageFlags(*messagePtr, item); } return true; } void MixedMaildirStore::Private::updateContextHashes(const QString &oldPath, const QString &newPath) { //qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "oldPath=" << oldPath << "newPath=" << newPath; const QString oldSubDirPath = Maildir::subDirPathForFolderPath(oldPath); const QString newSubDirPath = Maildir::subDirPathForFolderPath(newPath); MBoxHash mboxes; MBoxHash::const_iterator mboxIt = mMBoxes.constBegin(); MBoxHash::const_iterator mboxEndIt = mMBoxes.constEnd(); for (; mboxIt != mboxEndIt; ++mboxIt) { QString key = mboxIt.key(); MBoxPtr mboxPtr = mboxIt.value(); if (key == oldPath) { key = newPath; } else if (key.startsWith(oldSubDirPath)) { if (mboxPtr->hasIndexData() || mboxPtr->mRevision > 0) { key.replace(oldSubDirPath, newSubDirPath); } else { // if there is no index data yet, just discard this context key.clear(); } } if (!key.isEmpty()) { mboxPtr->updatePath(key); mboxes.insert(key, mboxPtr); } } //qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "mbox: old keys=" << mMBoxes.keys() << "new keys" << mboxes.keys(); mMBoxes = mboxes; MaildirHash maildirs; MaildirHash::const_iterator mdIt = mMaildirs.constBegin(); MaildirHash::const_iterator mdEndIt = mMaildirs.constEnd(); for (; mdIt != mdEndIt; ++mdIt) { QString key = mdIt.key(); MaildirPtr mdPtr = mdIt.value(); if (key == oldPath) { key = newPath; } else if (key.startsWith(oldSubDirPath)) { if (mdPtr->hasIndexData()) { key.replace(oldSubDirPath, newSubDirPath); } else { // if there is no index data yet, just discard this context key.clear(); } } if (!key.isEmpty()) { mdPtr->updatePath(key); maildirs.insert(key, mdPtr); } } //qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "maildir: old keys=" << mMaildirs.keys() << "new keys" << maildirs.keys(); mMaildirs = maildirs; } bool MixedMaildirStore::Private::visit(FileStore::Job *job) { const QString message = i18nc("@info:status", "Unhandled operation %1", QLatin1String(job->metaObject()->className())); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << message; q->notifyError(FileStore::Job::InvalidJobContext, message); return false; } bool MixedMaildirStore::Private::visit(FileStore::CollectionCreateJob *job) { QString path; QString errorText; const FolderType folderType = folderForCollection(job->targetParent(), path, errorText); if (folderType == InvalidFolder) { errorText = i18nc("@info:status", "Cannot create folder %1 inside folder %2", job->collection().name(), job->targetParent().name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } const QString collectionName = job->collection().name().remove(QDir::separator()); Maildir md; if (folderType == MBoxFolder) { const QString subDirPath = Maildir::subDirPathForFolderPath(path); const QDir dir(subDirPath); const QFileInfo dirInfo(dir, collectionName); if (dirInfo.exists() && !dirInfo.isDir()) { errorText = i18nc("@info:status", "Cannot create folder %1 inside folder %2", job->collection().name(), job->targetParent().name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType << ", dirInfo exists and it not a dir"; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } if (!dir.mkpath(collectionName)) { errorText = i18nc("@info:status", "Cannot create folder %1 inside folder %2", job->collection().name(), job->targetParent().name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType << ", mkpath failed"; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } md = Maildir(dirInfo.absoluteFilePath(), false); if (!md.create()) { errorText = i18nc("@info:status", "Cannot create folder %1 inside folder %2", job->collection().name(), job->targetParent().name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType << ", maildir create failed"; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } const MaildirPtr mdPtr(new MaildirContext(md)); mMaildirs.insert(md.path(), mdPtr); } else { Maildir parentMd(path, folderType == TopLevelFolder); if (parentMd.addSubFolder(collectionName).isEmpty()) { errorText = i18nc("@info:status", "Cannot create folder %1 inside folder %2", job->collection().name(), job->targetParent().name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } md = Maildir(parentMd.subFolder(collectionName)); const MaildirPtr mdPtr(new MaildirContext(md)); mMaildirs.insert(md.path(), mdPtr); } Collection collection = job->collection(); collection.setRemoteId(collectionName); collection.setName(collectionName); collection.setParentCollection(job->targetParent()); fillMaildirCollectionDetails(md, collection); q->notifyCollectionsProcessed(Collection::List() << collection); return true; } bool MixedMaildirStore::Private::visit(FileStore::CollectionDeleteJob *job) { QString path; QString errorText; const FolderType folderType = folderForCollection(job->collection(), path, errorText); if (folderType == InvalidFolder) { errorText = i18nc("@info:status", "Cannot remove folder %1 from folder %2", job->collection().name(), job->collection().parentCollection().name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } QString parentPath; const FolderType parentFolderType = folderForCollection(job->collection().parentCollection(), parentPath, errorText); if (parentFolderType == InvalidFolder) { errorText = i18nc("@info:status", "Cannot remove folder %1 from folder %2", job->collection().name(), job->collection().parentCollection().name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "Parent FolderType=" << parentFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } if (folderType == MBoxFolder) { if (!QFile::remove(path)) { errorText = i18nc("@info:status", "Cannot remove folder %1 from folder %2", job->collection().name(), job->collection().parentCollection().name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } } else { if (!QDir(path).removeRecursively()) { errorText = i18nc("@info:status", "Cannot remove folder %1 from folder %2", job->collection().name(), job->collection().parentCollection().name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } } const QString subDirPath = Maildir::subDirPathForFolderPath(path); QDir(subDirPath).removeRecursively(); q->notifyCollectionsProcessed(Collection::List() << job->collection()); return true; } bool MixedMaildirStore::Private::visit(FileStore::CollectionFetchJob *job) { QString path; QString errorText; const FolderType folderType = folderForCollection(job->collection(), path, errorText); if (folderType == InvalidFolder) { qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "collection:" << job->collection(); q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } Collection::List collections; Collection collection = job->collection(); if (job->type() == FileStore::CollectionFetchJob::Base) { collection.setName(collection.remoteId()); if (folderType == MBoxFolder) { MBoxPtr mbox; MBoxHash::const_iterator findIt = mMBoxes.constFind(path); if (findIt == mMBoxes.constEnd()) { mbox = MBoxPtr(new MBoxContext); if (!mbox->load(path)) { errorText = i18nc("@info:status", "Failed to load MBox folder %1", path); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "collection=" << collection; q->notifyError(FileStore::Job::InvalidJobContext, errorText); // TODO should be a different error code return false; } mbox->mCollection = collection; mMBoxes.insert(path, mbox); } else { mbox = findIt.value(); } fillMBoxCollectionDetails(mbox, collection); } else { const Maildir md(path, folderType == TopLevelFolder); fillMaildirCollectionDetails(md, collection); } collections << collection; } else { // if the base is an mbox, use its sub folder dir like a top level maildir if (folderType == MBoxFolder) { path = Maildir::subDirPathForFolderPath(path); } const Maildir md(path, folderType != MaildirFolder); fillMaildirTreeDetails(md, collection, collections, job->type() == FileStore::CollectionFetchJob::Recursive); } if (!collections.isEmpty()) { q->notifyCollectionsProcessed(collections); } return true; } static Collection updateMBoxCollectionTree(const Collection &collection, const Collection &oldParent, const Collection &newParent) { if (collection == oldParent) { return newParent; } if (collection == Collection::root()) { return collection; } Collection updatedCollection = collection; updatedCollection.setParentCollection(updateMBoxCollectionTree(collection.parentCollection(), oldParent, newParent)); return updatedCollection; } bool MixedMaildirStore::Private::visit(FileStore::CollectionModifyJob *job) { const Collection collection = job->collection(); const QString collectionName = collection.name().remove(QDir::separator()); // we also only do renames if (collection.remoteId() == collection.name()) { qCWarning(MIXEDMAILDIRRESOURCE_LOG) << "CollectionModifyJob with name still identical to remoteId. Ignoring"; return true; } QString path; QString errorText; const FolderType folderType = folderForCollection(collection, path, errorText); if (folderType == InvalidFolder) { errorText = i18nc("@info:status", "Cannot rename folder %1", collection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } const QFileInfo fileInfo(path); const QFileInfo subDirInfo = Maildir::subDirPathForFolderPath(path); QDir parentDir(path); parentDir.cdUp(); const QFileInfo targetFileInfo(parentDir, collectionName); const QFileInfo targetSubDirInfo = Maildir::subDirPathForFolderPath(targetFileInfo.absoluteFilePath()); if (targetFileInfo.exists() || (subDirInfo.exists() && targetSubDirInfo.exists())) { errorText = i18nc("@info:status", "Cannot rename folder %1", collection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } // if there is an index, make sure it is read before renaming // do not rename index as it could already be out of date bool indexInvalidated = false; if (folderType == MBoxFolder) { // TODO would be nice if getOrCreateMBoxPtr() could be used instead, like below for Maildir MBoxPtr mbox; MBoxHash::const_iterator findIt = mMBoxes.constFind(path); if (findIt == mMBoxes.constEnd()) { mbox = MBoxPtr(new MBoxContext); if (!mbox->load(path)) { qCWarning(MIXEDMAILDIRRESOURCE_LOG) << "Failed to load mbox" << path; } mbox->mCollection = collection; mMBoxes.insert(path, mbox); } else { mbox = findIt.value(); } mbox->readIndexData(); indexInvalidated = mbox->hasIndexData(); } else if (folderType == MaildirFolder) { MaildirPtr md = getOrCreateMaildirPtr(path, false); md->readIndexData(); indexInvalidated = md->hasIndexData(); } if (!parentDir.rename(fileInfo.absoluteFilePath(), targetFileInfo.fileName())) { errorText = i18nc("@info:status", "Cannot rename folder %1", collection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } if (subDirInfo.exists()) { if (!parentDir.rename(subDirInfo.absoluteFilePath(), targetSubDirInfo.fileName())) { errorText = i18nc("@info:status", "Cannot rename folder %1", collection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); // try to recover the previous rename parentDir.rename(targetFileInfo.absoluteFilePath(), fileInfo.fileName()); return false; } } // update context hashes updateContextHashes(fileInfo.absoluteFilePath(), targetFileInfo.absoluteFilePath()); Collection renamedCollection = collection; // when renaming top level folder, change path of store if (folderType == TopLevelFolder) { // backup caches, setTopLevelCollection() clears them const MBoxHash mboxes = mMBoxes; const MaildirHash maildirs = mMaildirs; q->setPath(targetFileInfo.absoluteFilePath()); // restore caches mMBoxes = mboxes; mMaildirs = maildirs; renamedCollection = q->topLevelCollection(); } else { renamedCollection.setRemoteId(collectionName); renamedCollection.setName(collectionName); } // update collections in MBox contexts so they stay usable for purge Q_FOREACH (const MBoxPtr &mbox, mMBoxes) { if (mbox->mCollection.isValid()) { MBoxPtr updatedMBox = mbox; updatedMBox->mCollection = updateMBoxCollectionTree(mbox->mCollection, collection, renamedCollection); } } if (indexInvalidated) { const QVariant var = QVariant::fromValue(Collection::List() << renamedCollection); job->setProperty("onDiskIndexInvalidated", var); } q->notifyCollectionsProcessed(Collection::List() << renamedCollection); return true; } bool MixedMaildirStore::Private::visit(FileStore::CollectionMoveJob *job) { QString errorText; const Collection moveCollection = job->collection(); const Collection targetCollection = job->targetParent(); QString movePath; const FolderType moveFolderType = folderForCollection(moveCollection, movePath, errorText); if (moveFolderType == InvalidFolder || moveFolderType == TopLevelFolder) { errorText = i18nc("@info:status", "Cannot move folder %1 from folder %2 to folder %3", moveCollection.name(), moveCollection.parentCollection().name(), targetCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << moveFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } // qCDebug(MIXEDMAILDIR_LOG) << "moveCollection" << moveCollection.remoteId() // << "movePath=" << movePath // << "moveType=" << moveFolderType; QString targetPath; const FolderType targetFolderType = folderForCollection(targetCollection, targetPath, errorText); if (targetFolderType == InvalidFolder) { errorText = i18nc("@info:status", "Cannot move folder %1 from folder %2 to folder %3", moveCollection.name(), moveCollection.parentCollection().name(), targetCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << targetFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } // qCDebug(MIXEDMAILDIR_LOG) << "targetCollection" << targetCollection.remoteId() // << "targetPath=" << targetPath // << "targetType=" << targetFolderType; const QFileInfo targetSubDirInfo(Maildir::subDirPathForFolderPath(targetPath)); // if target is not the top level folder, make sure the sub folder directory exists if (targetFolderType != TopLevelFolder) { if (!targetSubDirInfo.exists()) { QDir topDir(q->path()); if (!topDir.mkpath(targetSubDirInfo.absoluteFilePath())) { errorText = i18nc("@info:status", "Cannot move folder %1 from folder %2 to folder %3", moveCollection.name(), moveCollection.parentCollection().name(), targetCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "MoveFolderType=" << moveFolderType << "TargetFolderType=" << targetFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } } } bool indexInvalidated = false; QString movedPath; if (moveFolderType == MBoxFolder) { // TODO would be nice if getOrCreateMBoxPtr() could be used instead, like below for Maildir MBoxPtr mbox; MBoxHash::const_iterator findIt = mMBoxes.constFind(movePath); if (findIt == mMBoxes.constEnd()) { mbox = MBoxPtr(new MBoxContext); if (!mbox->load(movePath)) { qCWarning(MIXEDMAILDIRRESOURCE_LOG) << "Failed to load mbox" << movePath; } mbox->mCollection = moveCollection; mMBoxes.insert(movePath, mbox); } else { mbox = findIt.value(); } mbox->readIndexData(); indexInvalidated = mbox->hasIndexData(); const QFileInfo moveFileInfo(movePath); const QFileInfo moveSubDirInfo(Maildir::subDirPathForFolderPath(movePath)); const QFileInfo targetFileInfo(targetPath); QDir targetDir(targetFolderType == TopLevelFolder ? targetPath : Maildir::subDirPathForFolderPath(targetPath)); if (targetDir.exists(moveFileInfo.fileName()) || !targetDir.rename(moveFileInfo.absoluteFilePath(), moveFileInfo.fileName())) { errorText = i18nc("@info:status", "Cannot move folder %1 from folder %2 to folder %3", moveCollection.name(), moveCollection.parentCollection().name(), targetCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "MoveFolderType=" << moveFolderType << "TargetFolderType=" << targetFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } if (moveSubDirInfo.exists()) { if (targetDir.exists(moveSubDirInfo.fileName()) || !targetDir.rename(moveSubDirInfo.absoluteFilePath(), moveSubDirInfo.fileName())) { errorText = i18nc("@info:status", "Cannot move folder %1 from folder %2 to folder %3", moveCollection.name(), moveCollection.parentCollection().name(), targetCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "MoveFolderType=" << moveFolderType << "TargetFolderType=" << targetFolderType; // try to revert the other rename QDir sourceDir(moveFileInfo.absolutePath()); sourceDir.cdUp(); sourceDir.rename(targetFileInfo.absoluteFilePath(), moveFileInfo.fileName()); q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } } movedPath = QFileInfo(targetDir, moveFileInfo.fileName()).absoluteFilePath(); } else { MaildirPtr md = getOrCreateMaildirPtr(movePath, false); md->readIndexData(); indexInvalidated = md->hasIndexData(); Maildir moveMd(movePath, false); // for moving purpose we can treat the MBox target's subDirPath like a top level maildir Maildir targetMd; if (targetFolderType == MBoxFolder) { targetMd = Maildir(targetSubDirInfo.absoluteFilePath(), true); } else { targetMd = Maildir(targetPath, targetFolderType == TopLevelFolder); } if (!moveMd.moveTo(targetMd)) { errorText = i18nc("@info:status", "Cannot move folder %1 from folder %2 to folder %3", moveCollection.name(), moveCollection.parentCollection().name(), targetCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "MoveFolderType=" << moveFolderType << "TargetFolderType=" << targetFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } movedPath = targetMd.subFolder(moveCollection.remoteId()).path(); } // update context hashes updateContextHashes(movePath, movedPath); Collection movedCollection = moveCollection; movedCollection.setParentCollection(targetCollection); // update collections in MBox contexts so they stay usable for purge Q_FOREACH (const MBoxPtr &mbox, mMBoxes) { if (mbox->mCollection.isValid()) { MBoxPtr updatedMBox = mbox; updatedMBox->mCollection = updateMBoxCollectionTree(mbox->mCollection, moveCollection, movedCollection); } } if (indexInvalidated) { const QVariant var = QVariant::fromValue(Collection::List() << movedCollection); job->setProperty("onDiskIndexInvalidated", var); } q->notifyCollectionsProcessed(Collection::List() << movedCollection); return true; } bool MixedMaildirStore::Private::visit(FileStore::ItemCreateJob *job) { QString path; QString errorText; const FolderType folderType = folderForCollection(job->collection(), path, errorText); if (folderType == InvalidFolder || folderType == TopLevelFolder) { errorText = i18nc("@info:status", "Cannot add emails to folder %1", job->collection().name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } Item item = job->item(); if (folderType == MBoxFolder) { MBoxPtr mbox; MBoxHash::const_iterator findIt = mMBoxes.constFind(path); if (findIt == mMBoxes.constEnd()) { mbox = MBoxPtr(new MBoxContext); if (!mbox->load(path)) { errorText = i18nc("@info:status", "Cannot add emails to folder %1", job->collection().name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } mbox->mCollection = job->collection(); mMBoxes.insert(path, mbox); } else { mbox = findIt.value(); } // make sure to read the index (if available) before modifying the data, which would // make the index invalid mbox->readIndexData(); // if there is index data now, we let the job creator know that the on-disk index // became invalid if (mbox->hasIndexData()) { const QVariant var = QVariant::fromValue(Collection::List() << job->collection()); job->setProperty("onDiskIndexInvalidated", var); } qint64 result = mbox->appendEntry(item.payload()); if (result < 0) { errorText = i18nc("@info:status", "Cannot add emails to folder %1", job->collection().name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } mbox->save(); item.setRemoteId(QString::number(result)); } else { MaildirPtr mdPtr; MaildirHash::const_iterator findIt = mMaildirs.constFind(path); if (findIt == mMaildirs.constEnd()) { mdPtr = MaildirPtr(new MaildirContext(path, false)); mMaildirs.insert(path, mdPtr); } else { mdPtr = findIt.value(); } // make sure to read the index (if available) before modifying the data, which would // make the index invalid mdPtr->readIndexData(); // if there is index data now, we let the job creator know that the on-disk index // became invalid if (mdPtr->hasIndexData()) { const QVariant var = QVariant::fromValue(Collection::List() << job->collection()); job->setProperty("onDiskIndexInvalidated", var); } const QString result = mdPtr->addEntry(item.payload()->encodedContent()); if (result.isEmpty()) { errorText = i18nc("@info:status", "Cannot add emails to folder %1", job->collection().name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } item.setRemoteId(result); } item.setParentCollection(job->collection()); q->notifyItemsProcessed(Item::List() << item); return true; } bool MixedMaildirStore::Private::visit(FileStore::ItemDeleteJob *job) { const Item item = job->item(); const Collection collection = item.parentCollection(); QString path; QString errorText; const FolderType folderType = folderForCollection(collection, path, errorText); if (folderType == InvalidFolder || folderType == TopLevelFolder) { errorText = i18nc("@info:status", "Cannot remove emails from folder %1", collection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } if (folderType == MBoxFolder) { MBoxPtr mbox; MBoxHash::const_iterator findIt = mMBoxes.constFind(path); if (findIt == mMBoxes.constEnd()) { mbox = MBoxPtr(new MBoxContext); if (!mbox->load(path)) { errorText = i18nc("@info:status", "Cannot remove emails from folder %1", collection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } mMBoxes.insert(path, mbox); } else { mbox = findIt.value(); } bool ok = false; qint64 offset = item.remoteId().toLongLong(&ok); if (!ok || offset < 0 || !mbox->isValidOffset(offset)) { errorText = i18nc("@info:status", "Cannot remove emails from folder %1", collection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } mbox->mCollection = collection; mbox->deleteEntry(offset); job->setProperty("compactStore", true); } else { MaildirPtr mdPtr; MaildirHash::const_iterator findIt = mMaildirs.constFind(path); if (findIt == mMaildirs.constEnd()) { mdPtr = MaildirPtr(new MaildirContext(path, false)); mMaildirs.insert(path, mdPtr); } else { mdPtr = findIt.value(); } // make sure to read the index (if available) before modifying the data, which would // make the index invalid mdPtr->readIndexData(); // if there is index data now, we let the job creator know that the on-disk index // became invalid if (mdPtr->hasIndexData()) { const QVariant var = QVariant::fromValue(Collection::List() << collection); job->setProperty("onDiskIndexInvalidated", var); } if (!mdPtr->removeEntry(item.remoteId())) { errorText = i18nc("@info:status", "Cannot remove emails from folder %1", collection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } } q->notifyItemsProcessed(Item::List() << item); return true; } bool MixedMaildirStore::Private::visit(FileStore::ItemFetchJob *job) { ItemFetchScope scope = job->fetchScope(); const bool includeBody = scope.fullPayload() || scope.payloadParts().contains(MessagePart::Body); const bool includeHeaders = scope.payloadParts().contains(MessagePart::Header) || scope.payloadParts().contains(MessagePart::Envelope); const bool fetchItemsBatch = !job->requestedItems().isEmpty(); const bool fetchSingleItem = job->collection().remoteId().isEmpty() && !fetchItemsBatch; const Collection collection = fetchItemsBatch ? job->requestedItems().at(0).parentCollection() : fetchSingleItem ? job->item().parentCollection() : job->collection(); QString path; QString errorText; Q_ASSERT(!collection.remoteId().isEmpty()); const FolderType folderType = folderForCollection(collection, path, errorText); if (folderType == InvalidFolder) { qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "collection:" << job->collection(); q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } if (folderType == MBoxFolder) { MBoxHash::iterator findIt = mMBoxes.find(path); if (findIt == mMBoxes.end() || (!fetchSingleItem && !fetchItemsBatch)) { MBoxPtr mbox = findIt != mMBoxes.end() ? findIt.value() : MBoxPtr(new MBoxContext); if (!mbox->load(path)) { errorText = i18nc("@info:status", "Failed to load MBox folder %1", path); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "collection=" << collection; q->notifyError(FileStore::Job::InvalidJobContext, errorText); // TODO should be a different error code if (findIt != mMBoxes.end()) { mMBoxes.erase(findIt); } return false; } if (findIt == mMBoxes.end()) { findIt = mMBoxes.insert(path, mbox); } } Item::List items; if (fetchSingleItem) { items << job->item(); } else if (fetchItemsBatch) { items = job->requestedItems(); } else { listCollection(job, findIt.value(), collection, items); } Item::List::iterator it = items.begin(); Item::List::iterator endIt = items.end(); for (; it != endIt; ++it) { if (!fillItem(findIt.value(), includeHeaders, includeBody, *it)) { const QString errorText = i18nc("@info:status", "Error while reading mails from folder %1", collection.name()); q->notifyError(FileStore::Job::InvalidJobContext, errorText); // TODO should be a different error code qCCritical(MIXEDMAILDIRRESOURCE_LOG) << "Failed to read item" << (*it).remoteId() << "in MBox file" << path; return false; } } if (!items.isEmpty()) { q->notifyItemsProcessed(items); } } else { MaildirPtr mdPtr; MaildirHash::const_iterator mdIt = mMaildirs.constFind(path); if (mdIt == mMaildirs.constEnd()) { mdPtr = MaildirPtr(new MaildirContext(path, folderType == TopLevelFolder)); mMaildirs.insert(path, mdPtr); } else { mdPtr = mdIt.value(); } if (!mdPtr->isValid(errorText)) { errorText = i18nc("@info:status", "Failed to load Maildirs folder %1", path); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "collection=" << collection; q->notifyError(FileStore::Job::InvalidJobContext, errorText); // TODO should be a different error code return false; } Item::List items; if (fetchSingleItem) { items << job->item(); } else if (fetchItemsBatch) { items = job->requestedItems(); } else { listCollection(job, mdPtr, collection, items); } Item::List::iterator it = items.begin(); Item::List::iterator endIt = items.end(); for (; it != endIt; ++it) { if (!fillItem(mdPtr, includeHeaders, includeBody, *it)) { const QString errorText = i18nc("@info:status", "Error while reading mails from folder %1", collection.name()); q->notifyError(FileStore::Job::InvalidJobContext, errorText); // TODO should be a different error code qCCritical(MIXEDMAILDIRRESOURCE_LOG) << "Failed to read item" << (*it).remoteId() << "in Maildir" << path; return false; } } if (!items.isEmpty()) { q->notifyItemsProcessed(items); } } return true; } bool MixedMaildirStore::Private::visit(FileStore::ItemModifyJob *job) { const QSet parts = job->parts(); bool payloadChanged = false; bool flagsChanged = false; for (const QByteArray &part : parts) { if (part.startsWith("PLD:")) { payloadChanged = true; } if (part.contains("FLAGS")) { flagsChanged = true; } } const bool nothingChanged = (!payloadChanged && !flagsChanged); const bool payloadChangedButIgnored = payloadChanged && job->ignorePayload(); const bool ignoreModifyIfValid = nothingChanged || (payloadChangedButIgnored && !flagsChanged); Item item = job->item(); const Collection collection = item.parentCollection(); QString path; QString errorText; const FolderType folderType = folderForCollection(collection, path, errorText); if (folderType == InvalidFolder || folderType == TopLevelFolder) { errorText = i18nc("@info:status", "Cannot modify emails in folder %1", collection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } if (folderType == MBoxFolder) { MBoxPtr mbox; MBoxHash::const_iterator findIt = mMBoxes.constFind(path); if (findIt == mMBoxes.constEnd()) { mbox = MBoxPtr(new MBoxContext); if (!mbox->load(path)) { errorText = i18nc("@info:status", "Cannot modify emails in folder %1", collection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } mMBoxes.insert(path, mbox); } else { mbox = findIt.value(); } bool ok = false; qint64 offset = item.remoteId().toLongLong(&ok); if (!ok || offset < 0 || !mbox->isValidOffset(offset)) { errorText = i18nc("@info:status", "Cannot modify emails in folder %1", collection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } // if we can ignore payload, or we have nothing else to change, then we are finished if (ignoreModifyIfValid) { qCDebug(MIXEDMAILDIR_LOG) << "ItemModifyJob for item" << item.remoteId() << "in collection" << collection.remoteId() << "skipped: nothing of interest changed (" << nothingChanged << ") or only payload changed but should be ignored (" << (payloadChanged && !flagsChanged && job->ignorePayload()) << "). Modified parts:" << parts; q->notifyItemsProcessed(Item::List() << job->item()); return true; } // mbox can only change payload, ignore any other change if (!payloadChanged) { q->notifyItemsProcessed(Item::List() << item); return true; } // make sure to read the index (if available) before modifying the data, which would // make the index invalid mbox->readIndexData(); // if there is index data now, we let the job creator know that the on-disk index // became invalid if (mbox->hasIndexData()) { const QVariant var = QVariant::fromValue(Collection::List() << collection); job->setProperty("onDiskIndexInvalidated", var); } qint64 newOffset = mbox->appendEntry(item.payload()); if (newOffset < 0) { errorText = i18nc("@info:status", "Cannot modify emails in folder %1", collection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } if (newOffset > 0) { mbox->mCollection = collection; mbox->deleteEntry(offset); job->setProperty("compactStore", true); } mbox->save(); item.setRemoteId(QString::number(newOffset)); } else { MaildirPtr mdPtr; MaildirHash::const_iterator findIt = mMaildirs.constFind(path); if (findIt == mMaildirs.constEnd()) { mdPtr = MaildirPtr(new MaildirContext(path, false)); mMaildirs.insert(path, mdPtr); } else { mdPtr = findIt.value(); } if (!mdPtr->isValidEntry(item.remoteId())) { errorText = i18nc("@info:status", "Cannot modify emails in folder %1", collection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } // if we can ignore payload, or we have nothing else to change, then we are finished if (ignoreModifyIfValid) { qCDebug(MIXEDMAILDIR_LOG) << "ItemModifyJob for item" << item.remoteId() << "in collection" << collection.remoteId() << "skipped: nothing of interest changed (" << nothingChanged << ") or only payload changed but should be ignored (" << (payloadChanged && !flagsChanged && job->ignorePayload()) << "). Modified parts:" << parts; q->notifyItemsProcessed(Item::List() << job->item()); return true; } // make sure to read the index (if available) before modifying the data, which would // make the index invalid mdPtr->readIndexData(); // if there is index data now, we let the job creator know that the on-disk index // became invalid if (mdPtr->hasIndexData()) { const QVariant var = QVariant::fromValue(Collection::List() << collection); job->setProperty("onDiskIndexInvalidated", var); } QString newKey = item.remoteId(); if (flagsChanged) { Maildir md(mdPtr->maildir()); newKey = md.changeEntryFlags(item.remoteId(), item.flags()); if (newKey.isEmpty()) { errorText = i18nc("@info:status", "Cannot modify emails in folder %1. %2", collection.name(), md.lastError()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << folderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } item.setRemoteId(newKey); } if (payloadChanged) { mdPtr->writeEntry(newKey, item.payload()->encodedContent()); } } q->notifyItemsProcessed(Item::List() << item); return true; } bool MixedMaildirStore::Private::visit(FileStore::ItemMoveJob *job) { QString errorText; QString sourcePath; const Collection sourceCollection = job->item().parentCollection(); const FolderType sourceFolderType = folderForCollection(sourceCollection, sourcePath, errorText); if (sourceFolderType == InvalidFolder || sourceFolderType == TopLevelFolder) { errorText = i18nc("@info:status", "Cannot move emails from folder %1", sourceCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << sourceFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } // qCDebug(MIXEDMAILDIR_LOG) << "sourceCollection" << sourceCollection.remoteId() // << "sourcePath=" << sourcePath // << "sourceType=" << sourceFolderType; QString targetPath; const Collection targetCollection = job->targetParent(); const FolderType targetFolderType = folderForCollection(targetCollection, targetPath, errorText); if (targetFolderType == InvalidFolder || targetFolderType == TopLevelFolder) { errorText = i18nc("@info:status", "Cannot move emails to folder %1", targetCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << targetFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } // qCDebug(MIXEDMAILDIR_LOG) << "targetCollection" << targetCollection.remoteId() // << "targetPath=" << targetPath // << "targetType=" << targetFolderType; Item item = job->item(); if (sourceFolderType == MBoxFolder) { /* qCDebug(MIXEDMAILDIR_LOG) << "source is MBox";*/ bool ok = false; quint64 offset = item.remoteId().toULongLong(&ok); if (!ok) { errorText = i18nc("@info:status", "Cannot move emails from folder %1", sourceCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << sourceFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } MBoxPtr mbox; MBoxHash::const_iterator findIt = mMBoxes.constFind(sourcePath); if (findIt == mMBoxes.constEnd()) { mbox = MBoxPtr(new MBoxContext); if (!mbox->load(sourcePath)) { errorText = i18nc("@info:status", "Cannot move emails to folder %1", sourceCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << sourceFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } mbox->mCollection = sourceCollection; mMBoxes.insert(sourcePath, mbox); } else { mbox = findIt.value(); } if (!mbox->isValidOffset(offset)) { errorText = i18nc("@info:status", "Cannot move emails from folder %1", sourceCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << sourceFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } if (!item.hasPayload() || !item.loadedPayloadParts().contains(MessagePart::Body)) { if (!fillItem(mbox, true, true, item)) { errorText = i18nc("@info:status", "Cannot move email from folder %1", sourceCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << sourceFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } } Collection::List collections; // make sure to read the index (if available) before modifying the data, which would // make the index invalid mbox->readIndexData(); if (mbox->hasIndexData()) { collections << sourceCollection; } if (targetFolderType == MBoxFolder) { /* qCDebug(MIXEDMAILDIR_LOG) << "target is MBox";*/ MBoxPtr targetMBox; MBoxHash::const_iterator findIt = mMBoxes.constFind(targetPath); if (findIt == mMBoxes.constEnd()) { targetMBox = MBoxPtr(new MBoxContext); if (!targetMBox->load(targetPath)) { errorText = i18nc("@info:status", "Cannot move emails to folder %1", targetCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << targetFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } targetMBox->mCollection = targetCollection; mMBoxes.insert(targetPath, targetMBox); } else { targetMBox = findIt.value(); } // make sure to read the index (if available) before modifying the data, which would // make the index invalid targetMBox->readIndexData(); // if there is index data now, we let the job creator know that the on-disk index // became invalid if (targetMBox->hasIndexData()) { collections << targetCollection; } qint64 remoteId = targetMBox->appendEntry(item.payload()); if (remoteId < 0) { errorText = i18nc("@info:status", "Cannot move emails to folder %1", targetCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << targetFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } if (!targetMBox->save()) { errorText = i18nc("@info:status", "Cannot move emails to folder %1", targetCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << targetFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } item.setRemoteId(QString::number(remoteId)); } else { /* qCDebug(MIXEDMAILDIR_LOG) << "target is Maildir";*/ MaildirPtr targetMdPtr = getOrCreateMaildirPtr(targetPath, false); // make sure to read the index (if available) before modifying the data, which would // make the index invalid targetMdPtr->readIndexData(); // if there is index data now, we let the job creator know that the on-disk index // became invalid if (targetMdPtr->hasIndexData()) { collections << targetCollection; } const QString remoteId = targetMdPtr->addEntry(mbox->readRawEntry(offset)); if (remoteId.isEmpty()) { errorText = i18nc("@info:status", "Cannot move email from folder %1 to folder %2", sourceCollection.name(), targetCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "SourceFolderType=" << sourceFolderType << "TargetFolderType=" << targetFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } item.setRemoteId(remoteId); } if (!collections.isEmpty()) { const QVariant var = QVariant::fromValue(collections); job->setProperty("onDiskIndexInvalidated", var); } mbox->mCollection = sourceCollection; mbox->deleteEntry(offset); job->setProperty("compactStore", true); } else { /* qCDebug(MIXEDMAILDIR_LOG) << "source is Maildir";*/ MaildirPtr sourceMdPtr = getOrCreateMaildirPtr(sourcePath, false); if (!sourceMdPtr->isValidEntry(item.remoteId())) { errorText = i18nc("@info:status", "Cannot move email from folder %1", sourceCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << sourceFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } Collection::List collections; // make sure to read the index (if available) before modifying the data, which would // make the index invalid sourceMdPtr->readIndexData(); // if there is index data now, we let the job creator know that the on-disk index // became invalid if (sourceMdPtr->hasIndexData()) { collections << sourceCollection; } if (targetFolderType == MBoxFolder) { /* qCDebug(MIXEDMAILDIR_LOG) << "target is MBox";*/ if (!item.hasPayload() || !item.loadedPayloadParts().contains(MessagePart::Body)) { if (!fillItem(sourceMdPtr, true, true, item)) { errorText = i18nc("@info:status", "Cannot move email from folder %1", sourceCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << sourceFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } } MBoxPtr mbox; MBoxHash::const_iterator findIt = mMBoxes.constFind(targetPath); if (findIt == mMBoxes.constEnd()) { mbox = MBoxPtr(new MBoxContext); if (!mbox->load(targetPath)) { errorText = i18nc("@info:status", "Cannot move emails to folder %1", targetCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << targetFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } mbox->mCollection = targetCollection; mMBoxes.insert(targetPath, mbox); } else { mbox = findIt.value(); } // make sure to read the index (if available) before modifying the data, which would // make the index invalid mbox->readIndexData(); // if there is index data now, we let the job creator know that the on-disk index // became invalid if (mbox->hasIndexData()) { collections << targetCollection; } const qint64 remoteId = mbox->appendEntry(item.payload()); if (remoteId < 0) { errorText = i18nc("@info:status", "Cannot move emails to folder %1", targetCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "FolderType=" << targetFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } sourceMdPtr->removeEntry(item.remoteId()); mbox->save(); item.setRemoteId(QString::number(remoteId)); } else { /* qCDebug(MIXEDMAILDIR_LOG) << "target is Maildir";*/ MaildirPtr targetMdPtr = getOrCreateMaildirPtr(targetPath, false); // make sure to read the index (if available) before modifying the data, which would // make the index invalid targetMdPtr->readIndexData(); // if there is index data now, we let the job creator know that the on-disk index // became invalid if (targetMdPtr->hasIndexData()) { collections << targetCollection; } const QString remoteId = sourceMdPtr->moveEntryTo(item.remoteId(), *targetMdPtr); if (remoteId.isEmpty()) { errorText = i18nc("@info:status", "Cannot move email from folder %1 to folder %2", sourceCollection.name(), targetCollection.name()); qCCritical(MIXEDMAILDIRRESOURCE_LOG) << errorText << "SourceFolderType=" << sourceFolderType << "TargetFolderType=" << targetFolderType; q->notifyError(FileStore::Job::InvalidJobContext, errorText); return false; } item.setRemoteId(remoteId); } if (!collections.isEmpty()) { const QVariant var = QVariant::fromValue(collections); job->setProperty("onDiskIndexInvalidated", var); } } item.setParentCollection(targetCollection); q->notifyItemsProcessed(Item::List() << item); return true; } bool MixedMaildirStore::Private::visit(FileStore::StoreCompactJob *job) { Q_UNUSED(job); Collection::List collections; MBoxHash::const_iterator it = mMBoxes.constBegin(); MBoxHash::const_iterator endIt = mMBoxes.constEnd(); for (; it != endIt; ++it) { MBoxPtr mbox = it.value(); if (!mbox->hasDeletedOffsets()) { continue; } // make sure to read the index (if available) before modifying the data, which would // make the index invalid mbox->readIndexData(); QList movedEntries; const int result = mbox->purge(movedEntries); if (result > 0) { if (!movedEntries.isEmpty()) { qint64 revision = mbox->mCollection.remoteRevision().toLongLong(); qCDebug(MIXEDMAILDIRRESOURCE_LOG) << "purge of" << mbox->mCollection.name() << "caused item move: oldRevision=" << revision << "(stored)," << mbox->mRevision << "(local)"; revision = qMax(revision, mbox->mRevision) + 1; const QString remoteRevision = QString::number(revision); Collection collection = mbox->mCollection; collection.attribute(Collection::AddIfMissing)->setRemoteRevision(remoteRevision); q->notifyCollectionsProcessed(Collection::List() << collection); mbox->mCollection.setRemoteRevision(remoteRevision); mbox->mRevision = revision; } Item::List items; items.reserve(movedEntries.count()); Q_FOREACH (const KMBox::MBoxEntry::Pair &offsetPair, movedEntries) { const QString oldRemoteId(QString::number(offsetPair.first.messageOffset())); const QString newRemoteId(QString::number(offsetPair.second.messageOffset())); Item item; item.setRemoteId(oldRemoteId); item.setParentCollection(mbox->mCollection); item.attribute(Item::AddIfMissing)->setRemoteId(newRemoteId); items << item; } // if there is index data, we let the job creator know that the on-disk index // became invalid if (mbox->hasIndexData()) { collections << mbox->mCollection; } if (!items.isEmpty()) { q->notifyItemsProcessed(items); } } } if (!collections.isEmpty()) { const QVariant var = QVariant::fromValue(collections); job->setProperty("onDiskIndexInvalidated", var); } return true; } MixedMaildirStore::MixedMaildirStore() : FileStore::AbstractLocalStore() , d(new Private(this)) { } MixedMaildirStore::~MixedMaildirStore() { delete d; } void MixedMaildirStore::setTopLevelCollection(const Collection &collection) { QStringList contentMimeTypes; contentMimeTypes << Collection::mimeType(); Collection::Rights rights; // TODO check if read-only? rights = Collection::CanCreateCollection | Collection::CanChangeCollection | Collection::CanDeleteCollection; CachePolicy cachePolicy; cachePolicy.setInheritFromParent(false); cachePolicy.setLocalParts(QStringList() << QLatin1String(MessagePart::Envelope)); cachePolicy.setSyncOnDemand(true); cachePolicy.setCacheTimeout(1); Collection modifiedCollection = collection; modifiedCollection.setContentMimeTypes(contentMimeTypes); modifiedCollection.setRights(rights); modifiedCollection.setParentCollection(Collection::root()); modifiedCollection.setCachePolicy(cachePolicy); // clear caches d->mMBoxes.clear(); d->mMaildirs.clear(); FileStore::AbstractLocalStore::setTopLevelCollection(modifiedCollection); } void MixedMaildirStore::processJob(FileStore::Job *job) { if (!job->accept(d)) { // check that an error has been set if (job->error() == 0 || job->errorString().isEmpty()) { qCCritical(MIXEDMAILDIRRESOURCE_LOG) << "visitor did not set either error code or error string when returning false"; Q_ASSERT(job->error() == 0 || job->errorString().isEmpty()); } } else { // check that no error has been set if (job->error() != 0 || !job->errorString().isEmpty()) { qCCritical(MIXEDMAILDIRRESOURCE_LOG) << "visitor did set either error code or error string when returning true"; Q_ASSERT(job->error() != 0 || !job->errorString().isEmpty()); } } } void MixedMaildirStore::checkCollectionMove(FileStore::CollectionMoveJob *job, int &errorCode, QString &errorText) const { // check if the target is not the collection itself or one if its children Collection targetCollection = job->targetParent(); while (targetCollection.isValid()) { if (targetCollection == job->collection()) { errorCode = FileStore::Job::InvalidJobContext; errorText = i18nc("@info:status", "Cannot move folder %1 into one of its own subfolder tree", job->collection().name()); return; } targetCollection = targetCollection.parentCollection(); } } void MixedMaildirStore::checkItemCreate(FileStore::ItemCreateJob *job, int &errorCode, QString &errorText) const { if (!job->item().hasPayload()) { errorCode = FileStore::Job::InvalidJobContext; errorText = i18nc("@info:status", "Cannot add email to folder %1 because there is no email content", job->collection().name()); } } void MixedMaildirStore::checkItemModify(FileStore::ItemModifyJob *job, int &errorCode, QString &errorText) const { if (!job->ignorePayload() && !job->item().hasPayload()) { errorCode = FileStore::Job::InvalidJobContext; errorText = i18nc("@info:status", "Cannot modify email in folder %1 because there is no email content", job->item().parentCollection().name()); } } void MixedMaildirStore::checkItemFetch(FileStore::ItemFetchJob *job, int &errorCode, QString &errorText) const { Q_UNUSED(errorCode); Q_UNUSED(errorText); if (!job->requestedItems().isEmpty()) { // Requesting items - for (const Item &item : job->requestedItems()) { + const auto items = job->requestedItems(); + for (const Item &item : items) { const Collection coll = item.parentCollection(); Q_ASSERT(!coll.remoteId().isEmpty()); } } else { // Requesting an entire collection Q_ASSERT(!job->collection().remoteId().isEmpty()); } } diff --git a/resources/tomboynotes/o2/o2.cpp b/resources/tomboynotes/o2/o2.cpp index b07cc7f74..0e689ef53 100644 --- a/resources/tomboynotes/o2/o2.cpp +++ b/resources/tomboynotes/o2/o2.cpp @@ -1,466 +1,466 @@ #include #include #include #include #include #include #include #include #include #include #if QT_VERSION >= 0x050000 #include #include #else #include #include #endif #include "o2.h" #include "o2replyserver.h" #include "o0globals.h" #include "o0settingsstore.h" #include "debug.h" /// Parse JSON data into a QVariantMap static QVariantMap parseTokenResponse(const QByteArray &data) { #if QT_VERSION >= 0x050000 QJsonParseError err; QJsonDocument doc = QJsonDocument::fromJson(data, &err); if (err.error != QJsonParseError::NoError) { qCWarning(TOMBOYNOTESRESOURCE_LOG) << "parseTokenResponse: Failed to parse token response due to err:" << err.errorString(); return QVariantMap(); } if (!doc.isObject()) { qCWarning(TOMBOYNOTESRESOURCE_LOG) << "parseTokenResponse: Token response is not an object"; return QVariantMap(); } return doc.object().toVariantMap(); #else QScriptEngine engine; QScriptValue value = engine.evaluate("(" + QString(data) + QLatin1Char(')')); QScriptValueIterator it(value); QVariantMap map; while (it.hasNext()) { it.next(); map.insert(it.name(), it.value().toVariant()); } return map; #endif } /// Add query parameters to a query static void addQueryParametersToUrl(QUrl &url, const QList > ¶meters) { #if QT_VERSION < 0x050000 url.setQueryItems(parameters); #else QUrlQuery query(url); query.setQueryItems(parameters); url.setQuery(query); #endif } O2::O2(QObject *parent) : O0BaseAuth(parent) { manager_ = new QNetworkAccessManager(this); replyServer_ = new O2ReplyServer(this); grantFlow_ = GrantFlowAuthorizationCode; localhostPolicy_ = QLatin1String(O2_CALLBACK_URL); qRegisterMetaType("QNetworkReply::NetworkError"); connect(replyServer_, &O2ReplyServer::verificationReceived, this, &O2::onVerificationReceived); } O2::GrantFlow O2::grantFlow() const { return grantFlow_; } void O2::setGrantFlow(O2::GrantFlow value) { grantFlow_ = value; Q_EMIT grantFlowChanged(); } QString O2::username() const { return username_; } void O2::setUsername(const QString &value) { username_ = value; Q_EMIT usernameChanged(); } QString O2::password() const { return password_; } void O2::setPassword(const QString &value) { password_ = value; Q_EMIT passwordChanged(); } QString O2::scope() const { return scope_; } void O2::setScope(const QString &value) { scope_ = value; Q_EMIT scopeChanged(); } QString O2::requestUrl() const { return requestUrl_.toString(); } void O2::setRequestUrl(const QString &value) { requestUrl_ = QUrl(value); Q_EMIT requestUrlChanged(); } QString O2::tokenUrl() { return tokenUrl_.toString(); } void O2::setTokenUrl(const QString &value) { tokenUrl_ = QUrl(value); Q_EMIT tokenUrlChanged(); } QString O2::refreshTokenUrl() { return refreshTokenUrl_.toString(); } void O2::setRefreshTokenUrl(const QString &value) { refreshTokenUrl_ = QUrl(value); Q_EMIT refreshTokenUrlChanged(); } void O2::link() { qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::link"; if (linked()) { qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::link: Linked already"; Q_EMIT linkingSucceeded(); return; } setLinked(false); setToken(QLatin1String("")); setTokenSecret(QLatin1String("")); setExtraTokens(QVariantMap()); setRefreshToken(QString()); setExpires(0); if (grantFlow_ == GrantFlowAuthorizationCode) { // Start listening to authentication replies replyServer_->listen(QHostAddress::Any, localPort_); // Save redirect URI, as we have to reuse it when requesting the access token redirectUri_ = localhostPolicy_.arg(replyServer_->serverPort()); // Assemble initial authentication URL QList > parameters; parameters.append(qMakePair(QLatin1String(O2_OAUTH2_RESPONSE_TYPE), (grantFlow_ == GrantFlowAuthorizationCode) ? QLatin1String(O2_OAUTH2_GRANT_TYPE_CODE) : QLatin1String(O2_OAUTH2_GRANT_TYPE_TOKEN))); parameters.append(qMakePair(QLatin1String(O2_OAUTH2_CLIENT_ID), clientId_)); parameters.append(qMakePair(QLatin1String(O2_OAUTH2_REDIRECT_URI), redirectUri_)); parameters.append(qMakePair(QLatin1String(O2_OAUTH2_SCOPE), scope_)); parameters.append(qMakePair(QLatin1String(O2_OAUTH2_API_KEY), apiKey_)); // Show authentication URL with a web browser QUrl url(requestUrl_); addQueryParametersToUrl(url, parameters); qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::link: Emit openBrowser" << url.toString(); Q_EMIT openBrowser(url); } else if (grantFlow_ == GrantFlowResourceOwnerPasswordCredentials) { QList parameters; parameters.append(O0RequestParameter(O2_OAUTH2_CLIENT_ID, clientId_.toUtf8())); parameters.append(O0RequestParameter(O2_OAUTH2_CLIENT_SECRET, clientSecret_.toUtf8())); parameters.append(O0RequestParameter(O2_OAUTH2_USERNAME, username_.toUtf8())); parameters.append(O0RequestParameter(O2_OAUTH2_PASSWORD, password_.toUtf8())); parameters.append(O0RequestParameter(O2_OAUTH2_GRANT_TYPE, O2_OAUTH2_GRANT_TYPE_PASSWORD)); parameters.append(O0RequestParameter(O2_OAUTH2_SCOPE, scope_.toUtf8())); parameters.append(O0RequestParameter(O2_OAUTH2_API_KEY, apiKey_.toUtf8())); QByteArray payload = O0BaseAuth::createQueryParameters(parameters); QUrl url(tokenUrl_); QNetworkRequest tokenRequest(url); tokenRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/x-www-form-urlencoded")); QNetworkReply *tokenReply = manager_->post(tokenRequest, payload); connect(tokenReply, &QNetworkReply::finished, this, &O2::onTokenReplyFinished, Qt::QueuedConnection); connect(tokenReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onTokenReplyError(QNetworkReply::NetworkError)), Qt::QueuedConnection); } } void O2::unlink() { qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::unlink"; setLinked(false); setToken(QString()); setRefreshToken(QString()); setExpires(0); setExtraTokens(QVariantMap()); Q_EMIT linkingSucceeded(); } void O2::onVerificationReceived(const QMap &response) { qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::onVerificationReceived:" << response; qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::onVerificationReceived: Emitting closeBrowser()"; Q_EMIT closeBrowser(); - if (response.contains(QLatin1String("error"))) { + if (response.contains(QStringLiteral("error"))) { qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O2::onVerificationReceived: Verification failed: " << response; Q_EMIT linkingFailed(); return; } if (grantFlow_ == GrantFlowAuthorizationCode) { // Save access code setCode(response.value(QLatin1String(O2_OAUTH2_GRANT_TYPE_CODE))); // Exchange access code for access/refresh tokens QString query; if (!apiKey_.isEmpty()) { query = QString(QLatin1String("?") + QLatin1String(O2_OAUTH2_API_KEY) + QLatin1String("=") + apiKey_); } QNetworkRequest tokenRequest(QUrl(tokenUrl_.toString() + query)); tokenRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String(O2_MIME_TYPE_XFORM)); QMap parameters; parameters.insert(QLatin1String(O2_OAUTH2_GRANT_TYPE_CODE), code()); parameters.insert(QLatin1String(O2_OAUTH2_CLIENT_ID), clientId_); parameters.insert(QLatin1String(O2_OAUTH2_CLIENT_SECRET), clientSecret_); parameters.insert(QLatin1String(O2_OAUTH2_REDIRECT_URI), redirectUri_); parameters.insert(QLatin1String(O2_OAUTH2_GRANT_TYPE), QLatin1String(O2_AUTHORIZATION_CODE)); QByteArray data = buildRequestBody(parameters); QNetworkReply *tokenReply = manager_->post(tokenRequest, data); timedReplies_.add(tokenReply); connect(tokenReply, &QNetworkReply::finished, this, &O2::onTokenReplyFinished, Qt::QueuedConnection); connect(tokenReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onTokenReplyError(QNetworkReply::NetworkError)), Qt::QueuedConnection); } else { setToken(response.value(QLatin1String(O2_OAUTH2_ACCESS_TOKEN))); setRefreshToken(response.value(QLatin1String(O2_OAUTH2_REFRESH_TOKEN))); } } QString O2::code() const { QString key = QString::fromLatin1(O2_KEY_CODE).arg(clientId_); return store_->value(key); } void O2::setCode(const QString &c) { QString key = QString::fromLatin1(O2_KEY_CODE).arg(clientId_); store_->setValue(key, c); } void O2::onTokenReplyFinished() { qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::onTokenReplyFinished"; QNetworkReply *tokenReply = qobject_cast(sender()); #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) const auto networkError = tokenReply->error(); #else const auto networkError = tokenReply->networkError(); #endif if (networkError == QNetworkReply::NoError) { QByteArray replyData = tokenReply->readAll(); QVariantMap tokens = parseTokenResponse(replyData); // Check for mandatory tokens if (tokens.contains(QLatin1String(O2_OAUTH2_ACCESS_TOKEN))) { setToken(tokens.take(QLatin1String(O2_OAUTH2_ACCESS_TOKEN)).toString()); bool ok = false; int expiresIn = tokens.take(QLatin1String(O2_OAUTH2_EXPIRES_IN)).toInt(&ok); if (ok) { qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::onTokenReplyFinished: Token expires in" << expiresIn << "seconds"; setExpires(QDateTime::currentSecsSinceEpoch() + expiresIn); } setRefreshToken(tokens.take(QLatin1String(O2_OAUTH2_REFRESH_TOKEN)).toString()); setExtraTokens(tokens); timedReplies_.remove(tokenReply); setLinked(true); Q_EMIT linkingSucceeded(); } else { qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O2::onTokenReplyFinished: oauth_token missing from response" << replyData; Q_EMIT linkingFailed(); } } tokenReply->deleteLater(); } void O2::onTokenReplyError(QNetworkReply::NetworkError error) { QNetworkReply *tokenReply = qobject_cast(sender()); qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O2::onTokenReplyError: " << error << ": " << tokenReply->errorString(); qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::onTokenReplyError: " << tokenReply->readAll(); setToken(QString()); setRefreshToken(QString()); timedReplies_.remove(tokenReply); Q_EMIT linkingFailed(); } QByteArray O2::buildRequestBody(const QMap ¶meters) { QByteArray body; bool first = true; for (const QString &key : parameters.keys()) { if (first) { first = false; } else { body.append("&"); } QString value = parameters.value(key); body.append(QUrl::toPercentEncoding(key) + QStringLiteral("=").toUtf8() + QUrl::toPercentEncoding(value)); } return body; } int O2::expires() { const QString key = QString::fromLatin1(O2_KEY_EXPIRES).arg(clientId_); return store_->value(key).toInt(); } void O2::setExpires(int v) { const QString key = QString::fromLatin1(O2_KEY_EXPIRES).arg(clientId_); store_->setValue(key, QString::number(v)); } QString O2::refreshToken() { const QString key = QString::fromLatin1(O2_KEY_REFRESH_TOKEN).arg(clientId_); return store_->value(key); } void O2::setRefreshToken(const QString &v) { qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::setRefreshToken" << v.left(4) << "..."; QString key = QString::fromLatin1(O2_KEY_REFRESH_TOKEN).arg(clientId_); store_->setValue(key, v); } void O2::refresh() { qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::refresh: Token: ..." << refreshToken().right(7); if (refreshToken().isEmpty()) { qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O2::refresh: No refresh token"; onRefreshError(QNetworkReply::AuthenticationRequiredError); return; } if (refreshTokenUrl_.isEmpty()) { qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O2::refresh: Refresh token URL not set"; onRefreshError(QNetworkReply::AuthenticationRequiredError); return; } QNetworkRequest refreshRequest(refreshTokenUrl_); refreshRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String(O2_MIME_TYPE_XFORM)); QMap parameters; parameters.insert(QLatin1String(O2_OAUTH2_CLIENT_ID), clientId_); parameters.insert(QLatin1String(O2_OAUTH2_CLIENT_SECRET), clientSecret_); parameters.insert(QLatin1String(O2_OAUTH2_REFRESH_TOKEN), refreshToken()); parameters.insert(QLatin1String(O2_OAUTH2_GRANT_TYPE), QLatin1String(O2_OAUTH2_REFRESH_TOKEN)); QByteArray data = buildRequestBody(parameters); QNetworkReply *refreshReply = manager_->post(refreshRequest, data); timedReplies_.add(refreshReply); connect(refreshReply, &QNetworkReply::finished, this, &O2::onRefreshFinished, Qt::QueuedConnection); connect(refreshReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRefreshError(QNetworkReply::NetworkError)), Qt::QueuedConnection); } void O2::onRefreshFinished() { QNetworkReply *refreshReply = qobject_cast(sender()); #if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) const auto networkError = refreshReply->error(); #else const auto networkError = refreshReply->networkError(); #endif qCDebug(TOMBOYNOTESRESOURCE_LOG) << "O2::onRefreshFinished: Error" << (int)networkError << refreshReply->errorString(); if (networkError == QNetworkReply::NoError) { QByteArray reply = refreshReply->readAll(); QVariantMap tokens = parseTokenResponse(reply); setToken(tokens.value(QLatin1String(O2_OAUTH2_ACCESS_TOKEN)).toString()); setExpires(QDateTime::currentSecsSinceEpoch() + tokens.value(QLatin1String(O2_OAUTH2_EXPIRES_IN)).toInt()); setRefreshToken(tokens.value(QLatin1String(O2_OAUTH2_REFRESH_TOKEN)).toString()); timedReplies_.remove(refreshReply); setLinked(true); Q_EMIT linkingSucceeded(); Q_EMIT refreshFinished(QNetworkReply::NoError); qCDebug(TOMBOYNOTESRESOURCE_LOG) << " New token expires in" << expires() << "seconds"; } refreshReply->deleteLater(); } void O2::onRefreshError(QNetworkReply::NetworkError error) { QNetworkReply *refreshReply = qobject_cast(sender()); qCWarning(TOMBOYNOTESRESOURCE_LOG) << "O2::onRefreshError: " << error; unlink(); timedReplies_.remove(refreshReply); Q_EMIT refreshFinished(error); } QString O2::localhostPolicy() const { return localhostPolicy_; } void O2::setLocalhostPolicy(const QString &value) { localhostPolicy_ = value; } QString O2::apiKey() const { return apiKey_; } void O2::setApiKey(const QString &value) { apiKey_ = value; } QByteArray O2::replyContent() const { return replyServer_->replyContent(); } void O2::setReplyContent(const QByteArray &value) { replyServer_->setReplyContent(value); } bool O2::ignoreSslErrors() { return timedReplies_.ignoreSslErrors(); } void O2::setIgnoreSslErrors(bool ignoreSslErrors) { timedReplies_.setIgnoreSslErrors(ignoreSslErrors); }