diff --git a/kdepim-runtime.categories b/kdepim-runtime.categories --- a/kdepim-runtime.categories +++ b/kdepim-runtime.categories @@ -20,3 +20,4 @@ org.kde.pim.tomboynotesresource tomboynote resource (kdepim-runtime) org.kde.pim.invitationagent invitationagent agent (kdepim-runtime) org.kde.pim.fbresource facebook resource (kdepim-runtime) +org.kde.pim.ews exchange ews resource (kdepim-runtime) diff --git a/resources/CMakeLists.txt b/resources/CMakeLists.txt --- a/resources/CMakeLists.txt +++ b/resources/CMakeLists.txt @@ -64,3 +64,4 @@ add_subdirectory( vcard ) add_subdirectory( folderarchivesettings ) add_subdirectory( tomboynotes ) +add_subdirectory( ews ) diff --git a/resources/ews/CHANGELOG b/resources/ews/CHANGELOG new file mode 100644 --- /dev/null +++ b/resources/ews/CHANGELOG @@ -0,0 +1,38 @@ +#### Version 0.8.1 - TBD + + Bugfix release. + + Notable changes: + + * Improve collection tree retrieval: + - Fix full collection tree synchronisation for some servers (bug #2). + - Fix incremental collection tree synchronisation. + - Reduce the number of full collection tree retrievals by keeping the state + persistent. + + * Fix Qt 5.7 compatibility + + * Fix crash upon encountering a distribution list item (bug #7). + + * Improve behaviour in case of temporary server connection loss. + + * Add option to customise User Agent string sent to the server. + + * Add option to enable NTLMv2 authentication. + + * Allow specifying an empty domain name (bug #3). Requires KF 5.28 to have any + effect. + + * Fix dates when copying items from other resources to the Sent Items folder + (bug #5). + + * Improve batch item retrieval performance (will be effective with KDE + Applications 16.12). + + * Synchronise the collection tree upon connection. Fixes empty collection tree + after adding a new resource until a manual mail fetch is triggered (bug #15). + + +#### Version 0.8.0 - 2016-04-30 + + Initial release with functional mail and read-only calendar & tasks. diff --git a/resources/ews/CMakeLists.txt b/resources/ews/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/resources/ews/CMakeLists.txt @@ -0,0 +1,174 @@ +# +# 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. + +# Build a separate resource for sending e-mail. This is needed for KMail which assumes that +# a single resource is not able to both send and receive e-mail. +set(HAVE_SEPARATE_MTA_RESOURCE TRUE) + +# Pretend the inbox folder remote identifier is "INBOX" in order to satisfy the assumption +# that only such named folders should be monitored for filtering rules. +set(HAVE_INBOX_FILTERING_WORKAROUND TRUE) + +set(AKONADI_EWS_VERSION ${KDEPIM_RUNTIME_VERSION}) + +add_subdirectory(ewsclient) + +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config.h) + +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/ewsclient) +include_directories(${CMAKE_CURRENT_BINARY_DIR}/ewsclient) + +add_definitions(-DTRANSLATION_DOMAIN=\"akonadi_ews_resource\") + +set(ewsresource_SRCS + abchperson/ewsabchpersonhandler.cpp + abchperson/ewscreateabchpersonjob.cpp + abchperson/ewsfetchabchpersondetailjob.cpp + abchperson/ewsmodifyabchpersonjob.cpp + calendar/ewscalendarhandler.cpp + calendar/ewscreatecalendarjob.cpp + calendar/ewsfetchcalendardetailjob.cpp + calendar/ewsmodifycalendarjob.cpp + contact/ewscontacthandler.cpp + contact/ewscreatecontactjob.cpp + contact/ewsfetchcontactdetailjob.cpp + contact/ewsmodifycontactjob.cpp + mail/ewscreatemailjob.cpp + mail/ewsfetchmaildetailjob.cpp + mail/ewsmailhandler.cpp + mail/ewsmodifymailjob.cpp + tags/ewsakonaditagssyncjob.cpp + tags/ewsglobaltagsreadjob.cpp + tags/ewsglobaltagswritejob.cpp + tags/ewstagstore.cpp + tags/ewsupdateitemstagsjob.cpp + task/ewscreatetaskjob.cpp + task/ewsfetchtaskdetailjob.cpp + task/ewsmodifytaskjob.cpp + task/ewstaskhandler.cpp + configdialog.cpp + ewsautodiscoveryjob.cpp + ewscreateitemjob.cpp + ewsfetchfoldersjob.cpp + ewsfetchfoldersincrjob.cpp + ewsfetchitemsjob.cpp + ewsfetchitemdetailjob.cpp + ewsitemhandler.cpp + ewsmodifyitemjob.cpp + ewsmodifyitemflagsjob.cpp + ewsresource.cpp + ewssubscribedfoldersjob.cpp + ewssubscriptionmanager.cpp + ewssubscriptionwidget.cpp + progressdialog.cpp + settings.cpp) + +ecm_qt_declare_logging_category(ewsresource_SRCS + HEADER ewsres_debug.h + IDENTIFIER EWSRES_LOG + CATEGORY_NAME org.kde.pim.ews) +ecm_qt_declare_logging_category(ewsresource_SRCS + HEADER ewsres_agentif_debug.h + IDENTIFIER EWSRES_AGENTIF_LOG + CATEGORY_NAME org.kde.pim.ews.agentif) + + +ki18n_wrap_ui(ewsresource_SRCS configdialog.ui) + +kconfig_add_kcfg_files(ewsresource_SRCS settingsbase.kcfgc) +kcfg_generate_dbus_interface(${CMAKE_CURRENT_SOURCE_DIR}/ewsresource.kcfg org.kde.Akonadi.Ews.Settings) +qt5_add_dbus_adaptor(ewsresource_SRCS + ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.Ews.Settings.xml settings.h Settings +) + +qt5_generate_dbus_interface( ${CMAKE_CURRENT_SOURCE_DIR}/ewsresource.h org.kde.Akonadi.Ews.Resource.xml OPTIONS -a ) +qt5_add_dbus_adaptor(ewsresource_SRCS + ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.Ews.Resource.xml ewsresource.h EwsResource +) +qt5_generate_dbus_interface( ${CMAKE_CURRENT_SOURCE_DIR}/settings.h org.kde.Akonadi.Ews.Wallet.xml OPTIONS -a ) +qt5_add_dbus_adaptor(ewsresource_SRCS + ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.Ews.Wallet.xml settings.h Settings +) + + + +add_executable(akonadi_ews_resource ${ewsresource_SRCS}) + +target_link_libraries(akonadi_ews_resource + KF5::AkonadiAgentBase + KF5::AkonadiCore + KF5::AkonadiMime + KF5::KIOCore + KF5::Mime + KF5::CalendarCore + KF5::Contacts + KF5::Wallet + KF5::WidgetsAddons + KF5::I18n + KF5::WindowSystem + ewsclient) + +if (HAVE_SEPARATE_MTA_RESOURCE) + set(ewsmtaresource_SRCS + ewsmtaresource.cpp + mtaconfigdialog.cpp) + + ki18n_wrap_ui(ewsmtaresource_SRCS mtaconfigdialog.ui) + + kconfig_add_kcfg_files(ewsmtaresource_SRCS mtasettings.kcfgc) + kcfg_generate_dbus_interface(${CMAKE_CURRENT_SOURCE_DIR}/ewsmtaresource.kcfg org.kde.Akonadi.EwsMta.Settings) + qt5_add_dbus_adaptor(ewsmtaresource_SRCS + ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.EwsMta.Settings.xml mtasettings.h MtaSettings mtasettingsadaptor + ) + qt5_add_dbus_interface(ewsmtaresource_SRCS ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.Ews.Resource.xml resourceinterface) + + add_executable(akonadi_ewsmta_resource ${ewsmtaresource_SRCS}) + target_link_libraries(akonadi_ewsmta_resource + KF5::AkonadiAgentBase + KF5::AkonadiCore + KF5::AkonadiWidgets + KF5::Mime + KF5::KDELibs4Support) + + set(EWS_MTA_CAPABILITIES "X-EwsMailTransport") +else (HAVE_SEPARATE_MTA_RESOURCE) + set(EWS_MTA_CAPABILITIES "MailTransport,Synchronizable") +endif (HAVE_SEPARATE_MTA_RESOURCE) + +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/ewsresource.desktop.cmake ${CMAKE_CURRENT_BINARY_DIR}/ewsresource.desktop) + +if (BUILD_TESTING) + add_subdirectory(test) +endif (BUILD_TESTING) + + +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/ewsresource.desktop + DESTINATION "${CMAKE_INSTALL_PREFIX}/share/akonadi/agents" ) +install(TARGETS akonadi_ews_resource ${INSTALL_TARGETS_DEFAULT_ARGS}) + +if (HAVE_SEPARATE_MTA_RESOURCE) + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/ewsmtaresource.desktop + DESTINATION "${CMAKE_INSTALL_PREFIX}/share/akonadi/agents" ) + install(TARGETS akonadi_ewsmta_resource ${INSTALL_TARGETS_DEFAULT_ARGS}) +endif (HAVE_SEPARATE_MTA_RESOURCE) + +foreach(icon_resolution 16 22 24 32 48 64 72 96 128) + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/icons/akonadi-ews-${icon_resolution}.png + DESTINATION "${CMAKE_INSTALL_PREFIX}/share/icons/hicolor/${icon_resolution}x${icon_resolution}/apps/" + RENAME "akonadi-ews.png") +endforeach(icon_resolution) diff --git a/resources/ews/Messages.sh b/resources/ews/Messages.sh new file mode 100755 --- /dev/null +++ b/resources/ews/Messages.sh @@ -0,0 +1,3 @@ +#! /usr/bin/env bash +$EXTRACTRC *.ui *.kcfg >> rc.cpp +$XGETTEXT *.cpp -o $podir/akonadi_ews_resource.pot diff --git a/resources/ews/README.md b/resources/ews/README.md new file mode 100644 --- /dev/null +++ b/resources/ews/README.md @@ -0,0 +1,148 @@ +## Akonadi resource for Microsoft Exchange Web Services + +This Akonadi resource provides the ability to use Microsoft Exchange accounts +with KDE PIM applications like KMail and KOrganizer. + +It allows to use KDE PIM applications with Exchange mailboxes, for which the +server administrator has not enabled access using standard e-mail protocols +(IMAP, POP3 or SMTP). Additionally since the resource communicates with Exchange +using its native protocol (also used by Microsoft Outlook) it allows to use the +full capabilities of Exchange which are not accessible over IMAP or POP3. + +The aim of this project is to allow full management of Exchange mailboxes so +that the KDE PIM suite can become a drop-in replacement for Microsoft Outlook. + +Currently the EWS resource is an independent project, but in the long term the +plan is for this to become part of KDE PIM. + +### Current status + +The EWS resource is currently in preview state. It can be used as a daily driver +when it comes to e-mail. For other tasks like calendar interaction (scheduling +and accepting meetings) it is necessary to use wither OWA or Microsoft Outlook. + +### Supported features + +* E-mail reception and full mailbox access +* Message & folder operations (copying, moving, deleting) +* Sending e-mail through Exchange (by default Exchange doesn't use SMTP to send + messages from clients) +* Server-side tags +* Calendar view (read-only) + +### Planned features + +* Full calendar support (currently only read-only support is provided) +* Task support +* Server-side address autocompletion (will need to sort out some KDE PIM + limitations) +* Out-of-office status manipulation +* Server-side message filtering +* Access to additional mailboxes & shared calendars + +### Software requirements + +* Microsoft Exchange 2007 SP1 or later +* Qt 5.5 or later +* KDE Frameworks 5.17 or later (at least 5.19 is recommended when using NTLMv2 + authentication)* +* KDE PIM 15.04.0 or later + +\* Alternatively you can also recompile the `kio` package and backport the +following commits from [kio.git](https://quickgit.kde.org/?p=kio.git): + +* 5961ac8e Fix NTLMv2 stage 3 response creation +* 2f894291 Try NTLMv2 authentication if the server denies NTLMv1 +* f665dd30 Try multiple authentication methods in case of failures + +The last commit is only necessary if your system is also configured to use Kerberos. + +### Debugging + +#### Some Akonadi basics + +Akonadi runs every resource in a separate process, which communicates to +the Akonadi server over DBus. When multiple instances of the same resource (for +ex. multiple Exchange accounts) are present, each of them also runs in a +separate process. You can easily find out your EWS resource instances by +running `ps -fu $(whoami) | grep akonadi_ews_resource`. + +Each Akonadi resource has a unique identifier. It is composed automatically out +of the resource name and a number, which starts from 0 and is incremented with +every new resource of that type. Removing a resource and adding it again will +not reset the counter. For example the first instance of the EWS resource would +use the identidier `akonadi_ews_resource_0`. The actual resource identifier is +visible in the command line of the resource process after executing the `ps` +command above. + +#### Running the resource process in the terminal + +By default the resource processes are started by the Akonadi server in the +background. All the output is sent to the log (either the journal in case of +systemd or the ~/.xsession-errors otherwise). + +It is possible to run the resource process in the foreground in which case all +the logs will be visible in the active console. Such mode of operation is very +useful for debugging. + +To run the resource process interactively: + + 1. Learn the resource identifier (use the `ps` command above). + 2. Kill the resource process. This needs to be done a few times as Akonadi will + initially try to restart the resource. The easiest way is to execute + `while [ $(killall akonadi_ews_resource) ]; do sleep 1; done`. Warning, if + you have several instances of the Exchange resource (several accounts) this + command will terminate all of them. + 3. Execute the resource process interactively (substitute the identifier with + the one obtained in step 1: + `akonadi_ews_resource --identifier akonadi_ews_resource_0`. + +Once running interactively you can at any moment terminate it by pressing Ctrl+C +and start it again. + +In order to run the resource process in the background again you have to restart +Akonadi by executing `akonadictl restart`. + +#### Enabling additional debug messages. + +Currently there are two kinds of debug messages that can be enabled in addition +to the default ones: + + - Request information (log_ews_resource_request) - prints information about each + EWS request sent and the response received. + - Protocol information (log_ews_resource_proto) - dumps all request and + response data (XML). + +To enable these messages edit (or create) the file +`~/.config/QtProject/qtlogging.ini` and put the following lines inside: + +``` +[Rules] +log_ews_resource_request.debug=true +log_ews_resource_proto.debug=true +``` + +Setting each of them to `false` or adding a `#` character in front of the line +will disable each of the logging categories. + +When starting the resource interactively make sure to edit the file and adjust +the debug messages before actually starting the resource. + +#### Request dumps + +When the protocol information messages are enabled or when some request fails +the contents of the request and the associated response will be dumped into +files in your `/tmp/` directory. The names of the relevant files will be printed +in the log next to the information about the failed request. Attaching such +dumps can be useful for debugging, however before doing so please make sure they +don't contain any confidential information. + +### Reporting bugs + +Please report bugs on the GitHub project page. In most cases it will be a great +help when logs are provided. + +Note however that the logs may information from your e-mail that might include +company confidential information. Before sending any logs please review them and +make sure any such information like e-mail addresses, dates or subjects is +anonymized (i.e. replaced by some meaningless information). diff --git a/resources/ews/abchperson/ewsabchpersonhandler.h b/resources/ews/abchperson/ewsabchpersonhandler.h new file mode 100644 --- /dev/null +++ b/resources/ews/abchperson/ewsabchpersonhandler.h @@ -0,0 +1,45 @@ +/* + 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. +*/ + +#ifndef EWSABCHPERSONHANDLER_H +#define EWSABCHPERSONHANDLER_H + +#include "ewsitemhandler.h" + +class EwsAbchPersonHandler : public EwsItemHandler +{ +public: + EwsAbchPersonHandler(); + virtual ~EwsAbchPersonHandler(); + + virtual EwsFetchItemDetailJob *fetchItemDetailJob(EwsClient &client, QObject *parent, + const Akonadi::Collection &collection) override; + virtual void setSeenFlag(Akonadi::Item &item, bool value) override; + virtual QString mimeType() override; + virtual bool setItemPayload(Akonadi::Item &item, const EwsItem &ewsItem) override; + virtual EwsModifyItemJob *modifyItemJob(EwsClient &client, const QVector &items, + const QSet &parts, QObject *parent) override; + virtual EwsCreateItemJob *createItemJob(EwsClient &client, const Akonadi::Item &item, + const Akonadi::Collection &collection, + EwsTagStore *tagStore, EwsResource *parent) override; + static EwsItemHandler *factory(); +private: +}; + +#endif diff --git a/resources/ews/abchperson/ewsabchpersonhandler.cpp b/resources/ews/abchperson/ewsabchpersonhandler.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/abchperson/ewsabchpersonhandler.cpp @@ -0,0 +1,81 @@ +/* + 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 "ewsabchpersonhandler.h" + +#include + +#include "ewsfetchabchpersondetailjob.h" +#include "ewsmodifyabchpersonjob.h" +#include "ewscreateabchpersonjob.h" + +using namespace Akonadi; + +EwsAbchPersonHandler::EwsAbchPersonHandler() +{ +} + +EwsAbchPersonHandler::~EwsAbchPersonHandler() +{ +} + +EwsItemHandler *EwsAbchPersonHandler::factory() +{ + return new EwsAbchPersonHandler(); +} + +EwsFetchItemDetailJob *EwsAbchPersonHandler::fetchItemDetailJob(EwsClient &client, QObject *parent, + const Akonadi::Collection &collection) +{ + return new EwsFetchAbchContactDetailsJob(client, parent, collection); +} + +void EwsAbchPersonHandler::setSeenFlag(Item &item, bool value) +{ + Q_UNUSED(item) + Q_UNUSED(value) +} + +QString EwsAbchPersonHandler::mimeType() +{ + return KCalCore::Todo::todoMimeType(); +} + +bool EwsAbchPersonHandler::setItemPayload(Akonadi::Item &item, const EwsItem &ewsItem) +{ + Q_UNUSED(item) + Q_UNUSED(ewsItem) + + return true; +} + +EwsModifyItemJob *EwsAbchPersonHandler::modifyItemJob(EwsClient &client, const QVector &items, + const QSet &parts, QObject *parent) +{ + return new EwsModifyAbchPersonJob(client, items, parts, parent); +} + +EwsCreateItemJob *EwsAbchPersonHandler::createItemJob(EwsClient &client, const Akonadi::Item &item, + const Akonadi::Collection &collection, + EwsTagStore *tagStore, EwsResource *parent) +{ + return new EwsCreateAbchPersonJob(client, item, collection, tagStore, parent); +} + +EWS_DECLARE_ITEM_HANDLER(EwsAbchPersonHandler, EwsItemTypeAbchPerson) diff --git a/resources/ews/abchperson/ewscreateabchpersonjob.h b/resources/ews/abchperson/ewscreateabchpersonjob.h new file mode 100644 --- /dev/null +++ b/resources/ews/abchperson/ewscreateabchpersonjob.h @@ -0,0 +1,37 @@ +/* + 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. +*/ + +#ifndef EWSCREATEABCHPERSONJOB_H +#define EWSCREATEABCHPERSONJOB_H + +#include "ewscreateitemjob.h" + +class EwsCreateAbchPersonJob : public EwsCreateItemJob +{ + Q_OBJECT +public: + EwsCreateAbchPersonJob(EwsClient &client, const Akonadi::Item &item, + const Akonadi::Collection &collection, EwsTagStore *tagStore, EwsResource *parent); + virtual ~EwsCreateAbchPersonJob(); + virtual bool setSend(bool send = true) override; +protected: + virtual void doStart() override; +}; + +#endif diff --git a/resources/ews/abchperson/ewscreateabchpersonjob.cpp b/resources/ews/abchperson/ewscreateabchpersonjob.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/abchperson/ewscreateabchpersonjob.cpp @@ -0,0 +1,46 @@ +/* + 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 "ewscreateabchpersonjob.h" + +#include "ewsresource_debug.h" + +EwsCreateAbchPersonJob::EwsCreateAbchPersonJob(EwsClient &client, const Akonadi::Item &item, + const Akonadi::Collection &collection, EwsTagStore *tagStore, + EwsResource *parent) + : EwsCreateItemJob(client, item, collection, tagStore, parent) +{ +} +EwsCreateAbchPersonJob::~EwsCreateAbchPersonJob() +{ +} + +void EwsCreateAbchPersonJob::doStart() +{ + qCWarning(EWSRES_LOG) << QStringLiteral("Abch person item creation not implemented!"); + emitResult(); +} + +bool EwsCreateAbchPersonJob::setSend(bool send) +{ + Q_UNUSED(send) + + qCWarning(EWSRES_LOG) << QStringLiteral("Sending abch person items is not supported!"); + return false; +} diff --git a/resources/ews/abchperson/ewsfetchabchpersondetailjob.h b/resources/ews/abchperson/ewsfetchabchpersondetailjob.h new file mode 100644 --- /dev/null +++ b/resources/ews/abchperson/ewsfetchabchpersondetailjob.h @@ -0,0 +1,35 @@ +/* + 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. +*/ + +#ifndef EWSFETCHABCHPERSONDETAILJOB_H +#define EWSFETCHABCHPERSONDETAILJOB_H + +#include "ewsfetchitemdetailjob.h" + +class EwsFetchAbchContactDetailsJob : public EwsFetchItemDetailJob +{ + Q_OBJECT +public: + EwsFetchAbchContactDetailsJob(EwsClient &client, QObject *parent, const Akonadi::Collection &collection); + virtual ~EwsFetchAbchContactDetailsJob(); +protected: + virtual void processItems(const QList &responses) override; +}; + +#endif diff --git a/resources/ews/abchperson/ewsfetchabchpersondetailjob.cpp b/resources/ews/abchperson/ewsfetchabchpersondetailjob.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/abchperson/ewsfetchabchpersondetailjob.cpp @@ -0,0 +1,62 @@ +/* + 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 "ewsfetchabchpersondetailjob.h" + +#include "ewsitemshape.h" +#include "ewsgetitemrequest.h" +#include "ewsmailbox.h" +#include "ewsresource_debug.h" + +using namespace Akonadi; + +EwsFetchAbchContactDetailsJob::EwsFetchAbchContactDetailsJob(EwsClient &client, QObject *parent, const Akonadi::Collection &collection) + : EwsFetchItemDetailJob(client, parent, collection) +{ + EwsItemShape shape(EwsShapeIdOnly); + mRequest->setItemShape(shape); +} + + +EwsFetchAbchContactDetailsJob::~EwsFetchAbchContactDetailsJob() +{ +} + +void EwsFetchAbchContactDetailsJob::processItems(const QList &responses) +{ + Item::List::iterator it = mChangedItems.begin(); + + Q_FOREACH (const EwsGetItemRequest::Response &resp, responses) { + Item &item = *it; + + if (!resp.isSuccess()) { + qCWarningNC(EWSRES_LOG) << QStringLiteral("Failed to fetch item %1").arg(item.remoteId()); + continue; + } + + //const EwsItem &ewsItem = resp.item(); + + // TODO: Implement + + ++it; + } + + emitResult(); +} + diff --git a/resources/ews/abchperson/ewsmodifyabchpersonjob.h b/resources/ews/abchperson/ewsmodifyabchpersonjob.h new file mode 100644 --- /dev/null +++ b/resources/ews/abchperson/ewsmodifyabchpersonjob.h @@ -0,0 +1,35 @@ +/* + 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. +*/ + +#ifndef EWSMODIFYABCHPERSONJOB_H +#define EWSMODIFYABCHPERSONJOB_H + +#include "ewsmodifyitemjob.h" + +class EwsModifyAbchPersonJob : public EwsModifyItemJob +{ + Q_OBJECT +public: + EwsModifyAbchPersonJob(EwsClient &client, const Akonadi::Item::List &items, const QSet &parts, + QObject *parent); + virtual ~EwsModifyAbchPersonJob(); + virtual void start() override; +}; + +#endif diff --git a/resources/ews/abchperson/ewsmodifyabchpersonjob.cpp b/resources/ews/abchperson/ewsmodifyabchpersonjob.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/abchperson/ewsmodifyabchpersonjob.cpp @@ -0,0 +1,37 @@ +/* + 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 "ewsmodifyabchpersonjob.h" + +#include "ewsresource_debug.h" + +EwsModifyAbchPersonJob::EwsModifyAbchPersonJob(EwsClient &client, const Akonadi::Item::List &items, + const QSet &parts, QObject *parent) + : EwsModifyItemJob(client, items, parts, parent) +{ +} +EwsModifyAbchPersonJob::~EwsModifyAbchPersonJob() +{ +} + +void EwsModifyAbchPersonJob::start() +{ + qCWarning(EWSRES_LOG) << QStringLiteral("Abch person item modification not implemented!"); + emitResult(); +} diff --git a/resources/ews/calendar/ewscalendarhandler.h b/resources/ews/calendar/ewscalendarhandler.h new file mode 100644 --- /dev/null +++ b/resources/ews/calendar/ewscalendarhandler.h @@ -0,0 +1,45 @@ +/* + 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. +*/ + +#ifndef EWSCALENDARHANDLER_H +#define EWSCALENDARHANDLER_H + +#include "ewsitemhandler.h" + +class EwsCalendarHandler : public EwsItemHandler +{ +public: + EwsCalendarHandler(); + virtual ~EwsCalendarHandler(); + + virtual EwsFetchItemDetailJob *fetchItemDetailJob(EwsClient &client, QObject *parent, + const Akonadi::Collection &collection) override; + virtual void setSeenFlag(Akonadi::Item &item, bool value) override; + virtual QString mimeType() override; + virtual bool setItemPayload(Akonadi::Item &item, const EwsItem &ewsItem) override; + virtual EwsModifyItemJob *modifyItemJob(EwsClient &client, const QVector &items, + const QSet &parts, QObject *parent) override; + virtual EwsCreateItemJob *createItemJob(EwsClient &client, const Akonadi::Item &item, + const Akonadi::Collection &collection, + EwsTagStore *tagStore, EwsResource *parent) override; + static EwsItemHandler *factory(); +private: +}; + +#endif diff --git a/resources/ews/calendar/ewscalendarhandler.cpp b/resources/ews/calendar/ewscalendarhandler.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/calendar/ewscalendarhandler.cpp @@ -0,0 +1,81 @@ +/* + 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 "ewscalendarhandler.h" + +#include + +#include "ewsfetchcalendardetailjob.h" +#include "ewsmodifycalendarjob.h" +#include "ewscreatecalendarjob.h" + +using namespace Akonadi; + +EwsCalendarHandler::EwsCalendarHandler() +{ +} + +EwsCalendarHandler::~EwsCalendarHandler() +{ +} + +EwsItemHandler *EwsCalendarHandler::factory() +{ + return new EwsCalendarHandler(); +} + +EwsFetchItemDetailJob *EwsCalendarHandler::fetchItemDetailJob(EwsClient &client, QObject *parent, + const Akonadi::Collection &collection) +{ + return new EwsFetchCalendarDetailJob(client, parent, collection); +} + +void EwsCalendarHandler::setSeenFlag(Item &item, bool value) +{ + Q_UNUSED(item) + Q_UNUSED(value) +} + +QString EwsCalendarHandler::mimeType() +{ + return KCalCore::Event::eventMimeType(); +} + +bool EwsCalendarHandler::setItemPayload(Akonadi::Item &item, const EwsItem &ewsItem) +{ + Q_UNUSED(item) + Q_UNUSED(ewsItem) + + return true; +} + +EwsModifyItemJob *EwsCalendarHandler::modifyItemJob(EwsClient &client, const QVector &items, + const QSet &parts, QObject *parent) +{ + return new EwsModifyCalendarJob(client, items, parts, parent); +} + +EwsCreateItemJob *EwsCalendarHandler::createItemJob(EwsClient &client, const Akonadi::Item &item, + const Akonadi::Collection &collection, + EwsTagStore *tagStore, EwsResource *parent) +{ + return new EwsCreateCalendarJob(client, item, collection, tagStore, parent); +} + +EWS_DECLARE_ITEM_HANDLER(EwsCalendarHandler, EwsItemTypeCalendarItem) diff --git a/resources/ews/calendar/ewscreatecalendarjob.h b/resources/ews/calendar/ewscreatecalendarjob.h new file mode 100644 --- /dev/null +++ b/resources/ews/calendar/ewscreatecalendarjob.h @@ -0,0 +1,37 @@ +/* + 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. +*/ + +#ifndef EWSCREATECALENDARJOB_H +#define EWSCREATECALENDARJOB_H + +#include "ewscreateitemjob.h" + +class EwsCreateCalendarJob : public EwsCreateItemJob +{ + Q_OBJECT +public: + EwsCreateCalendarJob(EwsClient &client, const Akonadi::Item &item, + const Akonadi::Collection &collection, EwsTagStore *tagStore, EwsResource *parent); + virtual ~EwsCreateCalendarJob(); + virtual bool setSend(bool send = true) override; +protected: + virtual void doStart() override; +}; + +#endif diff --git a/resources/ews/calendar/ewscreatecalendarjob.cpp b/resources/ews/calendar/ewscreatecalendarjob.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/calendar/ewscreatecalendarjob.cpp @@ -0,0 +1,46 @@ +/* + 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 "ewscreatecalendarjob.h" + +#include "ewsresource_debug.h" + +EwsCreateCalendarJob::EwsCreateCalendarJob(EwsClient &client, const Akonadi::Item &item, + const Akonadi::Collection &collection, + EwsTagStore *tagStore, EwsResource *parent) + : EwsCreateItemJob(client, item, collection, tagStore, parent) +{ +} +EwsCreateCalendarJob::~EwsCreateCalendarJob() +{ +} + +void EwsCreateCalendarJob::doStart() +{ + qCWarning(EWSRES_LOG) << QStringLiteral("Calendar item creation not implemented!"); + emitResult(); +} + +bool EwsCreateCalendarJob::setSend(bool send) +{ + Q_UNUSED(send) + + qCWarning(EWSRES_LOG) << QStringLiteral("Sending calendar items is not supported!"); + return false; +} diff --git a/resources/ews/calendar/ewsfetchcalendardetailjob.h b/resources/ews/calendar/ewsfetchcalendardetailjob.h new file mode 100644 --- /dev/null +++ b/resources/ews/calendar/ewsfetchcalendardetailjob.h @@ -0,0 +1,37 @@ +/* + 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. +*/ + +#ifndef EWSFETCHCALENDARDETAILJOB_H +#define EWSFETCHCALENDARDETAILJOB_H + +#include "ewsfetchitemdetailjob.h" + +class EwsFetchCalendarDetailJob : public EwsFetchItemDetailJob +{ + Q_OBJECT +public: + EwsFetchCalendarDetailJob(EwsClient &client, QObject *parent, const Akonadi::Collection &collection); + virtual ~EwsFetchCalendarDetailJob(); +protected: + virtual void processItems(const QList &responses) override; +private Q_SLOTS: + void exceptionItemsFetched(KJob *job); +}; + +#endif diff --git a/resources/ews/calendar/ewsfetchcalendardetailjob.cpp b/resources/ews/calendar/ewsfetchcalendardetailjob.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/calendar/ewsfetchcalendardetailjob.cpp @@ -0,0 +1,226 @@ +/* + 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 "ewsfetchcalendardetailjob.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include "ewsresource_debug.h" +#include "ewsgetitemrequest.h" +#include "ewsitemshape.h" +#include "ewsmailbox.h" +#include "ewsoccurrence.h" + +using namespace Akonadi; + +EwsFetchCalendarDetailJob::EwsFetchCalendarDetailJob(EwsClient &client, QObject *parent, + const Collection &collection) + : EwsFetchItemDetailJob(client, parent, collection) +{ + EwsItemShape shape(EwsShapeIdOnly); +/* shape << EwsPropertyField(QStringLiteral("calendar:UID")); + shape << EwsPropertyField(QStringLiteral("item:Subject")); + shape << EwsPropertyField(QStringLiteral("item:Body")); + shape << EwsPropertyField(QStringLiteral("calendar:Organizer")); + shape << EwsPropertyField(QStringLiteral("calendar:RequiredAttendees")); + shape << EwsPropertyField(QStringLiteral("calendar:OptionalAttendees")); + shape << EwsPropertyField(QStringLiteral("calendar:Resources")); + shape << EwsPropertyField(QStringLiteral("calendar:Start")); + shape << EwsPropertyField(QStringLiteral("calendar:End")); + shape << EwsPropertyField(QStringLiteral("calendar:IsAllDayEvent")); + shape << EwsPropertyField(QStringLiteral("calendar:LegacyFreeBusyStatus")); + shape << EwsPropertyField(QStringLiteral("calendar:AppointmentSequenceNumber")); + shape << EwsPropertyField(QStringLiteral("calendar:IsRecurring")); + shape << EwsPropertyField(QStringLiteral("calendar:Recurrence")); + shape << EwsPropertyField(QStringLiteral("calendar:FirstOccurrence")); + shape << EwsPropertyField(QStringLiteral("calendar:LastOccurrence")); + shape << EwsPropertyField(QStringLiteral("calendar:ModifiedOccurrences")); + shape << EwsPropertyField(QStringLiteral("calendar:DeletedOccurrences")); + shape << EwsPropertyField(QStringLiteral("calendar:StartTimeZone")); + shape << EwsPropertyField(QStringLiteral("calendar:EndTimeZone")); + shape << EwsPropertyField(QStringLiteral("calendar:MyResponseType")); + shape << EwsPropertyField(QStringLiteral("item:HasAttachments")); + shape << EwsPropertyField(QStringLiteral("item:Attachments"));*/ + +// shape << EwsPropertyField(QStringLiteral("item:Attachments")); + shape << EwsPropertyField(QStringLiteral("calendar:ModifiedOccurrences")); + shape << EwsPropertyField(QStringLiteral("calendar:DeletedOccurrences")); + shape << EwsPropertyField(QStringLiteral("item:Body")); + shape << EwsPropertyField(QStringLiteral("item:Culture")); + shape << EwsPropertyField(QStringLiteral("item:MimeContent")); + shape << EwsPropertyField(QStringLiteral("item:Subject")); + shape << EwsPropertyField(QStringLiteral("calendar:TimeZone")); + mRequest->setItemShape(shape); +} + + +EwsFetchCalendarDetailJob::~EwsFetchCalendarDetailJob() +{ +} + +void EwsFetchCalendarDetailJob::processItems(const QList &responses) +{ + Item::List::iterator it = mChangedItems.begin(); + KCalCore::ICalFormat format; + + EwsId::List addItems; + + Q_FOREACH (const EwsGetItemRequest::Response &resp, responses) { + Item &item = *it; + + if (!resp.isSuccess()) { + qCWarningNC(EWSRES_LOG) << QStringLiteral("Failed to fetch item %1").arg(item.remoteId()); + continue; + } + + const EwsItem &ewsItem = resp.item(); + QString mimeContent = ewsItem[EwsItemFieldMimeContent].toString(); + KCalCore::Calendar::Ptr memcal(new KCalCore::MemoryCalendar(QTimeZone::utc())); + format.fromString(memcal, mimeContent); + qCDebugNC(EWSRES_LOG) << QStringLiteral("Found %1 events").arg(memcal->events().count()); + KCalCore::Incidence::Ptr incidence; + if (memcal->events().count() > 1) { + Q_FOREACH (KCalCore::Event::Ptr event, memcal->events()) { + qCDebugNC(EWSRES_LOG) << QString::number(event->recurrence()->recurrenceType(), 16) << event->recurrenceId() << event->recurrenceId().isValid(); + if (!event->recurrenceId().isValid()) { + incidence = event; + } + } + EwsOccurrence::List excList = ewsItem[EwsItemFieldModifiedOccurrences].value(); + Q_FOREACH (const EwsOccurrence &exc, excList) { + addItems.append(exc.itemId()); + } + } else if (memcal->events().count() == 1) { + incidence = memcal->events()[0]; + } + //KCalCore::Incidence::Ptr incidence(format.fromString(mimeContent)); + + if (incidence) { + QString msTz = ewsItem[EwsItemFieldTimeZone].toString(); + QString culture = ewsItem[EwsItemFieldCulture].toString(); + QDateTime dt(incidence->dtStart()); + if (dt.isValid()) { + incidence->setDtStart(dt); + } + if (incidence->type() == KCalCore::Incidence::TypeEvent) { + KCalCore::Event *event = reinterpret_cast(incidence.data()); + dt = event->dtEnd(); + if (dt.isValid()) { + event->setDtEnd(dt); + } + } + dt = incidence->recurrenceId(); + if (dt.isValid()) { + incidence->setRecurrenceId(dt); + } + + item.setPayload(incidence); + } + + it++; + } + + if (addItems.isEmpty()) { + emitResult(); + } else { + EwsGetItemRequest *req = new EwsGetItemRequest(mClient, this); + EwsItemShape shape(EwsShapeIdOnly); +// shape << EwsPropertyField(QStringLiteral("item:Attachments")); + shape << EwsPropertyField(QStringLiteral("item:Body")); + shape << EwsPropertyField(QStringLiteral("item:MimeContent")); + shape << EwsPropertyField(QStringLiteral("calendar:TimeZone")); + shape << EwsPropertyField(QStringLiteral("item:Culture")); + req->setItemShape(shape); + + req->setItemIds(addItems); + connect(req, SIGNAL(result(KJob*)), SLOT(exceptionItemsFetched(KJob*))); + req->start(); + } +} + +void EwsFetchCalendarDetailJob::exceptionItemsFetched(KJob *job) +{ + if (job->error()) { + setError(job->error()); + setErrorText(job->errorText()); + emitResult(); + return; + } + + EwsGetItemRequest *req = qobject_cast(job); + + if (!req) { + setError(1); + setErrorText(QStringLiteral("Job is not an instance of EwsGetItemRequest")); + emitResult(); + return; + } + + KCalCore::ICalFormat format; + Q_FOREACH (const EwsGetItemRequest::Response &resp, req->responses()) { + if (!resp.isSuccess()) { + qCWarningNC(EWSRES_LOG) << QStringLiteral("Failed to fetch item."); + continue; + } + const EwsItem &ewsItem = resp.item(); + + Item item(KCalCore::Event::eventMimeType()); + item.setParentCollection(mCollection); + EwsId id = ewsItem[EwsItemFieldItemId].value(); + item.setRemoteId(id.id()); + item.setRemoteRevision(id.changeKey()); + + QString mimeContent = ewsItem[EwsItemFieldMimeContent].toString(); + KCalCore::Calendar::Ptr memcal(new KCalCore::MemoryCalendar(QTimeZone::utc())); + format.fromString(memcal, mimeContent); + KCalCore::Incidence::Ptr incidence(memcal->events().last()); + incidence->clearRecurrence(); + + QString msTz = ewsItem[EwsItemFieldStartTimeZone].toString(); + QString culture = ewsItem[EwsItemFieldCulture].toString(); + QDateTime dt(incidence->dtStart()); + if (dt.isValid()) { + incidence->setDtStart(dt); + } + if (incidence->type() == KCalCore::Incidence::TypeEvent) { + KCalCore::Event *event = reinterpret_cast(incidence.data()); + dt = event->dtEnd(); + if (dt.isValid()) { + event->setDtEnd(dt); + } + } + dt = incidence->recurrenceId(); + if (dt.isValid()) { + incidence->setRecurrenceId(dt); + } + + item.setPayload(incidence); + + mChangedItems.append(item); + } + + emitResult(); +} diff --git a/resources/ews/calendar/ewsmodifycalendarjob.h b/resources/ews/calendar/ewsmodifycalendarjob.h new file mode 100644 --- /dev/null +++ b/resources/ews/calendar/ewsmodifycalendarjob.h @@ -0,0 +1,35 @@ +/* + 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. +*/ + +#ifndef EWSMODIFYCALENDARJOB_H +#define EWSMODIFYCALENDARJOB_H + +#include "ewsmodifyitemjob.h" + +class EwsModifyCalendarJob : public EwsModifyItemJob +{ + Q_OBJECT +public: + EwsModifyCalendarJob(EwsClient &client, const Akonadi::Item::List &items, const QSet &parts, + QObject *parent); + virtual ~EwsModifyCalendarJob(); + virtual void start() override; +}; + +#endif diff --git a/resources/ews/calendar/ewsmodifycalendarjob.cpp b/resources/ews/calendar/ewsmodifycalendarjob.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/calendar/ewsmodifycalendarjob.cpp @@ -0,0 +1,37 @@ +/* + 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 "ewsmodifycalendarjob.h" + +#include "ewsresource_debug.h" + +EwsModifyCalendarJob::EwsModifyCalendarJob(EwsClient &client, const Akonadi::Item::List &items, + const QSet &parts, QObject *parent) + : EwsModifyItemJob(client, items, parts, parent) +{ +} +EwsModifyCalendarJob::~EwsModifyCalendarJob() +{ +} + +void EwsModifyCalendarJob::start() +{ + qCWarning(EWSRES_LOG) << QStringLiteral("Calendar item modification not implemented!"); + emitResult(); +} diff --git a/resources/ews/config.h.cmake b/resources/ews/config.h.cmake new file mode 100644 --- /dev/null +++ b/resources/ews/config.h.cmake @@ -0,0 +1,10 @@ +#ifndef _CONFIG_H +#define _CONFIG_H + +#cmakedefine HAVE_SEPARATE_MTA_RESOURCE + +#cmakedefine HAVE_INBOX_FILTERING_WORKAROUND + +#cmakedefine AKONADI_EWS_VERSION "@AKONADI_EWS_VERSION@" + +#endif diff --git a/resources/ews/configdialog.h b/resources/ews/configdialog.h new file mode 100644 --- /dev/null +++ b/resources/ews/configdialog.h @@ -0,0 +1,74 @@ +/* + 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. +*/ + +#ifndef CONFIGDIALOG_H +#define CONFIGDIALOG_H + +#include + +#include + +class QDialogButtonBox; +class EwsResource; +class EwsClient; +namespace Ui +{ +class SetupServerView; +} +class KJob; +class EwsAutodiscoveryJob; +class EwsGetFolderRequest; +class ProgressDialog; +class EwsSubscriptionWidget; + +class ConfigDialog : public QDialog +{ + Q_OBJECT +public: + explicit ConfigDialog(EwsResource *parentResource, EwsClient &client, WId windowId); + virtual ~ConfigDialog(); +private Q_SLOTS: + void save(); + void autoDiscoveryFinished(KJob *job); + void tryConnectFinished(KJob *job); + void performAutoDiscovery(); + void autoDiscoveryCancelled(); + void tryConnectCancelled(); + void setAutoDiscoveryNeeded(); + void dialogAccepted(); + void enableTryConnect(); + void tryConnect(); + void userAgentChanged(int index); +private: + QString fullUsername() const; + + EwsResource *mParentResource; + KConfigDialogManager *mConfigManager; + Ui::SetupServerView *mUi; + + QDialogButtonBox *mButtonBox; + EwsAutodiscoveryJob *mAutoDiscoveryJob; + EwsGetFolderRequest *mTryConnectJob; + bool mAutoDiscoveryNeeded; + bool mTryConnectNeeded; + ProgressDialog *mProgressDialog; + EwsSubscriptionWidget *mSubWidget; +}; + +#endif diff --git a/resources/ews/configdialog.cpp b/resources/ews/configdialog.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/configdialog.cpp @@ -0,0 +1,380 @@ +/* + 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 "configdialog.h" + +#include +#include +#include +#include +#include + +#include +#include + +#include "ui_configdialog.h" +#include "settings.h" +#include "ewsresource.h" +#include "ewsautodiscoveryjob.h" +#include "ewsgetfolderrequest.h" +#include "progressdialog.h" +#include "ewssubscriptionwidget.h" + +typedef QPair StringPair; + +static const QVector userAgents = { + {QStringLiteral("Microsoft Outlook 2016"), QStringLiteral("Microsoft Office/16.0 (Windows NT 10.0; Microsoft Outlook 16.0.6326; Pro)")}, + {QStringLiteral("Microsoft Outlook 2013"), QStringLiteral("Microsoft Office/15.0 (Windows NT 6.1; Microsoft Outlook 15.0.4420; Pro)")}, + {QStringLiteral("Microsoft Outlook 2010"), QStringLiteral("Microsoft Office/14.0 (Windows NT 6.1; Microsoft Outlook 14.0.5128; Pro)")}, + {QStringLiteral("Microsoft Outlook 2011 for Mac"), QStringLiteral("MacOutlook/14.2.0.101115 (Intel Mac OS X 10.6.7)")}, + {QStringLiteral("Mozilla Thunderbird 38 for Windows (with ExQuilla)"), QStringLiteral("Mozilla/5.0 (Windows NT 6.1; WOW64; rv:38.0) Gecko/20100101 Thunderbird/38.2.0")}, + {QStringLiteral("Mozilla Thunderbird 38 for Linux (with ExQuilla)"), QStringLiteral("Mozilla/5.0 (X11; Linux x86_64; rv:38.0) Gecko/20100101 Thunderbird/38.2.0")}, + {QStringLiteral("Mozilla Thunderbird 38 for Mac (with ExQuilla)"), QStringLiteral("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:38.0) Gecko/20100101 Thunderbird/38.2.0")} +}; + +ConfigDialog::ConfigDialog(EwsResource *parentResource, EwsClient &client, WId wId) + : QDialog(), mParentResource(parentResource), mAutoDiscoveryNeeded(false), mTryConnectNeeded(false), + mProgressDialog(nullptr) +{ + if (wId) { + KWindowSystem::setMainWindow(this, wId); + } + + mButtonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + QWidget *mainWidget = new QWidget(this); + QVBoxLayout *mainLayout = new QVBoxLayout(); + setLayout(mainLayout); + mainLayout->addWidget(mainWidget); + QPushButton *okButton = mButtonBox->button(QDialogButtonBox::Ok); + okButton->setDefault(true); + okButton->setShortcut(Qt::CTRL | Qt::Key_Return); + connect(mButtonBox, &QDialogButtonBox::accepted, this, &ConfigDialog::dialogAccepted); + connect(mButtonBox, &QDialogButtonBox::rejected, this, &ConfigDialog::reject); + mainLayout->addWidget(mButtonBox); + + setWindowTitle(i18n("Microsoft Exchange Configuration")); + + mUi = new Ui::SetupServerView; + mUi->setupUi(mainWidget); + mUi->accountName->setText(parentResource->name()); + + mSubWidget = new EwsSubscriptionWidget(client, mParentResource->settings(), this); + mUi->subscriptionTabLayout->addWidget(mSubWidget); + + mConfigManager = new KConfigDialogManager(this, mParentResource->settings()); + mConfigManager->updateWidgets(); + switch (mParentResource->settings()->retrievalMethod()) { + case 0: + mUi->pollRadioButton->setChecked(true); + break; + case 1: + mUi->streamingRadioButton->setChecked(true); + break; + default: + break; + } + + const EwsServerVersion &serverVer = client.serverVersion(); + if (serverVer.isValid()) { + mUi->serverStatusText->setText(i18nc("Server status", "OK")); + mUi->serverVersionText->setText(serverVer.toString()); + } + + bool baseUrlEmpty = mUi->kcfg_BaseUrl->text().isEmpty(); + mButtonBox->button(QDialogButtonBox::Ok)->setEnabled(!baseUrlEmpty); + mUi->tryConnectButton->setEnabled(!baseUrlEmpty); + mTryConnectNeeded = baseUrlEmpty; + + QString password; + mParentResource->settings()->requestPassword(password, false); + mUi->passwordEdit->setText(password); + + int selectedIndex = -1; + int i = 0; + Q_FOREACH (const StringPair &item, userAgents) { + mUi->userAgentCombo->addItem(item.first, item.second); + if (mParentResource->settings()->userAgent() == item.second) { + selectedIndex = i; + } + i++; + } + mUi->userAgentCombo->addItem(i18nc("User Agent", "Custom")); + if (!mParentResource->settings()->userAgent().isEmpty()) { + mUi->userAgentGroupBox->setChecked(true); + mUi->userAgentCombo->setCurrentIndex(selectedIndex >= 0 ? selectedIndex : mUi->userAgentCombo->count() - 1); + mUi->userAgentEdit->setText(mParentResource->settings()->userAgent()); + } else { + mUi->userAgentCombo->setCurrentIndex(mUi->userAgentCombo->count()); + } + + QIcon ewsIcon = QIcon::fromTheme(QStringLiteral("akonadi-ews")); + mUi->aboutIconLabel->setPixmap(ewsIcon.pixmap(96, 96, QIcon::Normal, QIcon::On)); + mUi->aboutTextLabel->setText(i18nc("@info", "Akonadi Resource for Microsoft Exchange Web Services (EWS)")); + mUi->aboutCopyrightLabel->setText(i18nc("@info", "Copyright (c) Krzysztof Nowicki 2015-2016")); + mUi->aboutVersionLabel->setText(i18nc("@info", "Version %1", QString::fromLatin1(AKONADI_EWS_VERSION))); + mUi->aboutLicenseLabel->setText(i18nc("@info", "Distributed under the GNU Library General Public License version 2.0 or later.")); + mUi->aboutUrlLabel->setText(QStringLiteral("https://github.com/KrissN/akonadi-ews")); + + connect(okButton, &QPushButton::clicked, this, &ConfigDialog::save); + connect(mUi->autodiscoverButton, &QPushButton::clicked, this, &ConfigDialog::performAutoDiscovery); + connect(mUi->kcfg_Username, SIGNAL(textChanged(QString)), this, SLOT(setAutoDiscoveryNeeded())); + connect(mUi->passwordEdit, SIGNAL(textChanged(QString)), this, SLOT(setAutoDiscoveryNeeded())); + connect(mUi->kcfg_Domain, SIGNAL(textChanged(QString)), this, SLOT(setAutoDiscoveryNeeded())); + connect(mUi->kcfg_HasDomain, SIGNAL(toggled(bool)), this, SLOT(setAutoDiscoveryNeeded())); + connect(mUi->kcfg_Email, SIGNAL(textChanged(QString)), this, SLOT(setAutoDiscoveryNeeded())); + connect(mUi->kcfg_BaseUrl, SIGNAL(textChanged(QString)), this, SLOT(enableTryConnect())); + connect(mUi->tryConnectButton, &QPushButton::clicked, this, &ConfigDialog::tryConnect); + connect(mUi->userAgentCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(userAgentChanged(int))); + connect(mUi->clearFolderTreeSyncStateButton, &QPushButton::clicked, mParentResource, + &EwsResource::clearFolderTreeSyncState); + connect(mUi->clearFolderItemSyncStateButton, &QPushButton::clicked, mParentResource, + static_cast(&EwsResource::clearFolderSyncState)); +} + +ConfigDialog::~ConfigDialog() +{ + delete mUi; +} + +void ConfigDialog::save() +{ + mParentResource->setName(mUi->accountName->text()); + mConfigManager->updateSettings(); + if (mUi->pollRadioButton->isChecked()) { + mParentResource->settings()->setRetrievalMethod(0); + } else { + mParentResource->settings()->setRetrievalMethod(1); + } + + /* Erase the subscription id in case subscription is disabled or its parameters changed. This + * fill force creation of a new subscription. */ + if (!mSubWidget->subscriptionEnabled() || + (mSubWidget->subscribedList() != mParentResource->settings()->serverSubscriptionList())) { + mParentResource->settings()->setEventSubscriptionId(QString()); + mParentResource->settings()->setEventSubscriptionWatermark(QString()); + } + + mParentResource->settings()->setServerSubscription(mSubWidget->subscriptionEnabled()); + if (mSubWidget->subscribedListValid()) { + mParentResource->settings()->setServerSubscriptionList(mSubWidget->subscribedList()); + } + + if (mUi->userAgentGroupBox->isChecked()) { + mParentResource->settings()->setUserAgent(mUi->userAgentEdit->text()); + } else { + mParentResource->settings()->setUserAgent(QString()); + } + + mParentResource->settings()->setPassword(mUi->passwordEdit->text()); + mParentResource->settings()->save(); +} + +void ConfigDialog::performAutoDiscovery() +{ + mAutoDiscoveryJob = new EwsAutodiscoveryJob(mUi->kcfg_Email->text(), + fullUsername(), mUi->passwordEdit->text(), + mUi->userAgentGroupBox->isEnabled() ? mUi->userAgentEdit->text() : QString(), + mUi->kcfg_EnableNTLMv2->isChecked(), this); + connect(mAutoDiscoveryJob, &EwsAutodiscoveryJob::result, this, &ConfigDialog::autoDiscoveryFinished); + mProgressDialog = new ProgressDialog(this, ProgressDialog::AutoDiscovery); + connect(mProgressDialog, &QDialog::rejected, this, &ConfigDialog::autoDiscoveryCancelled); + mAutoDiscoveryJob->start(); + mProgressDialog->show(); +} + +void ConfigDialog::autoDiscoveryFinished(KJob *job) +{ + if (job->error() || job != mAutoDiscoveryJob) { + KMessageBox::error(this, job->errorText(), i18nc("Exchange server autodiscovery", "Autodiscovery failed")); + mProgressDialog->reject(); + } else { + mProgressDialog->accept(); + mUi->kcfg_BaseUrl->setText(mAutoDiscoveryJob->ewsUrl()); + } + mAutoDiscoveryJob->deleteLater(); + mAutoDiscoveryJob = nullptr; + mProgressDialog->deleteLater(); + mProgressDialog = nullptr; +} + +void ConfigDialog::tryConnectFinished(KJob *job) +{ + if (job->error() || job != mTryConnectJob) { + KMessageBox::error(this, job->errorText(), i18nc("Exchange server connection", "Connection failed")); + mUi->serverStatusText->setText(i18nc("Exchange server status", "Failed")); + mUi->serverVersionText->setText(i18nc("Exchange server version", "Unknown")); + mProgressDialog->reject(); + } else { + mUi->serverStatusText->setText(i18nc("Exchange server status", "OK")); + mUi->serverVersionText->setText(mTryConnectJob->serverVersion().toString()); + mProgressDialog->accept(); + } + //mTryConnectJob->deleteLater(); + mTryConnectJob = nullptr; + //mProgressDialog->deleteLater(); + mProgressDialog = nullptr; +} + +void ConfigDialog::autoDiscoveryCancelled() +{ + if (mAutoDiscoveryJob) { + mAutoDiscoveryJob->kill(); + } + mProgressDialog->deleteLater(); + mProgressDialog = nullptr; +} + +void ConfigDialog::tryConnectCancelled() +{ + if (mTryConnectJob) { + mTryConnectJob->kill(); + } + //mTryConnectJob->deleteLater(); + mTryConnectJob = nullptr; +} + +void ConfigDialog::setAutoDiscoveryNeeded() +{ + mAutoDiscoveryNeeded = true; + mTryConnectNeeded = true; + + /* Enable the OK button when at least the e-mail and username fields are set. Additionally if + * autodiscovery is disabled, enable the OK button only if the base URL is set. */ + bool okEnabled = !mUi->kcfg_Username->text().isEmpty() && !mUi->kcfg_Email->text().isEmpty(); + if (!mUi->kcfg_AutoDiscovery->isChecked() && mUi->kcfg_BaseUrl->text().isEmpty()) { + okEnabled = false; + } + mButtonBox->button(QDialogButtonBox::Ok)->setEnabled(okEnabled); +} + +QString ConfigDialog::fullUsername() const +{ + QString username = mUi->kcfg_Username->text(); + if (mUi->kcfg_HasDomain->isChecked()) { + username.prepend(mUi->kcfg_Domain->text() + QStringLiteral("\\")); + } + return username; +} + +void ConfigDialog::dialogAccepted() +{ + if (mUi->kcfg_AutoDiscovery->isChecked() && mAutoDiscoveryNeeded) { + mAutoDiscoveryJob = new EwsAutodiscoveryJob(mUi->kcfg_Email->text(), + fullUsername(), mUi->passwordEdit->text(), + mUi->userAgentGroupBox->isEnabled() ? mUi->userAgentEdit->text() : QString(), + mUi->kcfg_EnableNTLMv2->isChecked(), this); + connect(mAutoDiscoveryJob, &EwsAutodiscoveryJob::result, this, &ConfigDialog::autoDiscoveryFinished); + mProgressDialog = new ProgressDialog(this, ProgressDialog::AutoDiscovery); + connect(mProgressDialog, &QDialog::rejected, this, &ConfigDialog::autoDiscoveryCancelled); + mAutoDiscoveryJob->start(); + if (!mProgressDialog->exec()) { + if (KMessageBox::questionYesNo(this, + i18n("Autodiscovery failed. This can be caused by incorrect parameters. Do you still want to save your settings?"), + i18n("Exchange server autodiscovery")) == KMessageBox::Yes) { + accept(); + return; + } else { + return; + } + } + } + + if (mTryConnectNeeded) { + EwsClient cli; + cli.setUrl(mUi->kcfg_BaseUrl->text()); + cli.setCredentials(fullUsername(), mUi->passwordEdit->text()); + if (mUi->userAgentGroupBox->isChecked()) { + cli.setUserAgent(mUi->userAgentEdit->text()); + } + cli.setEnableNTLMv2(mUi->kcfg_EnableNTLMv2->isChecked()); + mTryConnectJob = new EwsGetFolderRequest(cli, this); + mTryConnectJob->setFolderShape(EwsFolderShape(EwsShapeIdOnly)); + mTryConnectJob->setFolderIds(EwsId::List() << EwsId(EwsDIdInbox)); + connect(mTryConnectJob, &EwsGetFolderRequest::result, this, &ConfigDialog::tryConnectFinished); + mProgressDialog = new ProgressDialog(this, ProgressDialog::TryConnect); + connect(mProgressDialog, &QDialog::rejected, this, &ConfigDialog::tryConnectCancelled); + mTryConnectJob->start(); + if (!mProgressDialog->exec()) { + if (KMessageBox::questionYesNo(this, + i18n("Connecting to Exchange failed. This can be caused by incorrect parameters. Do you still want to save your settings?"), + i18n("Exchange server connection")) == KMessageBox::Yes) { + accept(); + return; + } else { + return; + } + } else { + accept(); + } + } + + if (!mTryConnectNeeded && !mAutoDiscoveryNeeded) { + accept(); + } +} + +void ConfigDialog::enableTryConnect() +{ + mTryConnectNeeded = true; + bool baseUrlEmpty = mUi->kcfg_BaseUrl->text().isEmpty(); + + /* Enable the OK button when at least the e-mail and username fields are set. Additionally if + * autodiscovery is disabled, enable the OK button only if the base URL is set. */ + bool okEnabled = !mUi->kcfg_Username->text().isEmpty() && !mUi->kcfg_Email->text().isEmpty(); + if (!mUi->kcfg_AutoDiscovery->isChecked() && baseUrlEmpty) { + okEnabled = false; + } + mButtonBox->button(QDialogButtonBox::Ok)->setEnabled(okEnabled); + mUi->tryConnectButton->setEnabled(!baseUrlEmpty); +} + +void ConfigDialog::tryConnect() +{ + EwsClient cli; + cli.setUrl(mUi->kcfg_BaseUrl->text()); + cli.setCredentials(fullUsername(), mUi->passwordEdit->text()); + if (mUi->userAgentGroupBox->isChecked()) { + cli.setUserAgent(mUi->userAgentEdit->text()); + } + cli.setEnableNTLMv2(mUi->kcfg_EnableNTLMv2->isChecked()); + mTryConnectJob = new EwsGetFolderRequest(cli, this); + mTryConnectJob->setFolderShape(EwsFolderShape(EwsShapeIdOnly)); + mTryConnectJob->setFolderIds(EwsId::List() << EwsId(EwsDIdInbox)); + mProgressDialog = new ProgressDialog(this, ProgressDialog::TryConnect); + connect(mProgressDialog, &QDialog::rejected, this, &ConfigDialog::tryConnectCancelled); + mProgressDialog->show(); + if (!mTryConnectJob->exec()) { + mUi->serverStatusText->setText(i18nc("Exchange server status", "Failed")); + mUi->serverVersionText->setText(i18nc("Exchange server version", "Unknown")); + KMessageBox::error(this, mTryConnectJob->errorText(), i18n("Connection failed")); + } else { + mUi->serverStatusText->setText(i18nc("Exchange server status", "OK")); + mUi->serverVersionText->setText(mTryConnectJob->serverVersion().toString()); + } + mProgressDialog->hide(); +} + +void ConfigDialog::userAgentChanged(int) +{ + QString data = mUi->userAgentCombo->currentData().toString(); + mUi->userAgentEdit->setEnabled(data.isEmpty()); + if (!data.isEmpty()) { + mUi->userAgentEdit->setText(data); + } +} diff --git a/resources/ews/configdialog.ui b/resources/ews/configdialog.ui new file mode 100644 --- /dev/null +++ b/resources/ews/configdialog.ui @@ -0,0 +1,707 @@ + + + SetupServerView + + + + 0 + 0 + 602 + 527 + + + + EWS Configuration + + + + + + 0 + + + + General + + + + + + + 500 + 0 + + + + Account Information + + + + + + + 0 + + + 0 + + + 0 + + + + + Account Name: + + + + + + + This is the account name that will appear in all Akonadi applications + + + + + + + Email: + + + + + + + The e-mail address of this account + + + + + + + false + + + The Active Directory domain the user belongs to. + + + + + + + Username: + + + + + + + <html><head/><body><p>The username used to login to the Exchange server.</p><p><span style=" font-weight:600;">Note:</span> This can be left empty when using Kerberos authentication.</p></body></html> + + + + + + + Password: + + + + + + + <html><head/><body><p>The password used to login to the Exchange server.</p><p><span style=" font-weight:600;">Note:</span> This can be left empty when using Kerberos authentication.</p></body></html> + + + QLineEdit::Password + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + <html><head/><body><p>Attempts to find out the Microsoft Exchange server address relevant to the supplied e-mail address.</p></body></html> + + + Server Autodiscovery + + + true + + + + + + + Perform + + + + + + + false + + + Try connect + + + + + + + + + + false + + + EWS URL: + + + + + + + false + + + The Microsoft Exchange server URL used to send EWS requests. + + + + + + + + 0 + 0 + + + + Domain: + + + + + + + + + + <html><head/><body><p><span style=" font-weight:600;">Note:</span> Username and password are not needed (can be empty) when Kerberos is properly configured on your system.</p></body></html> + + + false + + + true + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Subscriptions + + + + + + Advanced + + + + + + Retrieval + + + + + + <html><head/><body><p>Periodically asks for new item change events.</p></body></html> + + + Po&ll for new messages + + + + + + + false + + + + 48 + + + 0 + + + 0 + + + 0 + + + + + minutes + + + 1 + + + 0.500000000000000 + + + 0.500000000000000 + + + 1.000000000000000 + + + + + + + Interval + + + + + + + + + + <html><head/><body><p>Streaming notifications behave similarly to push mode - a connection is kept alive and notifications about item changes are delivered instantly.</p><p><br/></p><p>This option requires Exchange server version 2010 SP1 or later.</p></body></html> + + + &Use streaming notifications + + + true + + + + + + + + + + Authentication + + + + + + <html><head/><body><p>By default KDE uses the less secure NTLMv1 authentication. Enabling this will cause the more secure NTLMv2 protocol to be used.</p><p>Some newer Microsoft Exchange servers are configured to only allow NTLMv2 authentication. In such case this option must be enabled in order to connect successfully.</p></body></html> + + + Enable NTLMv2 + + + + + + + + + + <html><head/><body><p>Some organizations restrict access to mail only to approved clients, such as Microsoft Outlook. On the server-side this is implemented by analyzing the User-Agent string and only allowing a list of approved clients.</p><p>This setting allows to emulate some well known clients as well as providing a custom User-Agent string.</p><p>Please be advised that bypasing company policies may result in personal consequences.</p></body></html> + + + Use custo&m User Agent + + + true + + + false + + + + + + + + + false + + + + + + + + + + Clear Synchronization State + + + + + + Resets synchronization state between Exchange and Akonadi. Use in case some items or folders are not showing up despite existing in Exchange. + + + true + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Clear Folder Tree State + + + + + + + Clear Folder Item State + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Status + + + + + + Server Status + + + + + + Status: + + + + + + + Unknown + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Version: + + + + + + + Unknown + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + About + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + ../../../../../usr/share/icons/hicolor/128x128/apps/akonadi-ews.png + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + 0 + 0 + + + + + + + true + + + + + + + + + + + + + + + + true + + + + + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + KLineEdit + QLineEdit +
klineedit.h
+
+
+ + tabWidget + accountName + kcfg_Email + kcfg_Domain + kcfg_Username + passwordEdit + kcfg_AutoDiscovery + autodiscoverButton + tryConnectButton + kcfg_BaseUrl + pollRadioButton + kcfg_PollInterval + streamingRadioButton + + + + + kcfg_AutoDiscovery + toggled(bool) + autodiscoverButton + setEnabled(bool) + + + 256 + 257 + + + 388 + 257 + + + + + kcfg_AutoDiscovery + toggled(bool) + serverUrlLabel + setDisabled(bool) + + + 256 + 257 + + + 95 + 297 + + + + + kcfg_AutoDiscovery + toggled(bool) + kcfg_BaseUrl + setDisabled(bool) + + + 256 + 257 + + + 311 + 297 + + + + + pollRadioButton + toggled(bool) + pollIntervalWidget + setEnabled(bool) + + + 260 + 89 + + + 260 + 149 + + + + + kcfg_HasDomain + toggled(bool) + kcfg_Domain + setEnabled(bool) + + + 87 + 172 + + + 322 + 172 + + + + +
diff --git a/resources/ews/contact/ewscontacthandler.h b/resources/ews/contact/ewscontacthandler.h new file mode 100644 --- /dev/null +++ b/resources/ews/contact/ewscontacthandler.h @@ -0,0 +1,45 @@ +/* + 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. +*/ + +#ifndef EWSCONTACTHANDLER_H +#define EWSCONTACTHANDLER_H + +#include "ewsitemhandler.h" + +class EwsContactHandler : public EwsItemHandler +{ +public: + EwsContactHandler(); + virtual ~EwsContactHandler(); + + virtual EwsFetchItemDetailJob *fetchItemDetailJob(EwsClient &client, QObject *parent, + const Akonadi::Collection &collection) override; + virtual void setSeenFlag(Akonadi::Item &item, bool value) override; + virtual QString mimeType() override; + virtual bool setItemPayload(Akonadi::Item &item, const EwsItem &ewsItem) override; + virtual EwsModifyItemJob *modifyItemJob(EwsClient &client, const QVector &items, + const QSet &parts, QObject *parent) override; + virtual EwsCreateItemJob *createItemJob(EwsClient &client, const Akonadi::Item &item, + const Akonadi::Collection &collection, + EwsTagStore *tagStore, EwsResource *parent) override; + static EwsItemHandler *factory(); +private: +}; + +#endif diff --git a/resources/ews/contact/ewscontacthandler.cpp b/resources/ews/contact/ewscontacthandler.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/contact/ewscontacthandler.cpp @@ -0,0 +1,82 @@ +/* + 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 "ewscontacthandler.h" + +#include +#include + +#include "ewsfetchcontactdetailjob.h" +#include "ewsmodifycontactjob.h" +#include "ewscreatecontactjob.h" + +using namespace Akonadi; + +EwsContactHandler::EwsContactHandler() +{ +} + +EwsContactHandler::~EwsContactHandler() +{ +} + +EwsItemHandler *EwsContactHandler::factory() +{ + return new EwsContactHandler(); +} + +EwsFetchItemDetailJob *EwsContactHandler::fetchItemDetailJob(EwsClient &client, QObject *parent, + const Akonadi::Collection &collection) +{ + return new EwsFetchContactDetailJob(client, parent, collection); +} + +void EwsContactHandler::setSeenFlag(Item &item, bool value) +{ + Q_UNUSED(item) + Q_UNUSED(value) +} + +QString EwsContactHandler::mimeType() +{ + return KContacts::Addressee::mimeType(); +} + +bool EwsContactHandler::setItemPayload(Akonadi::Item &item, const EwsItem &ewsItem) +{ + Q_UNUSED(item) + Q_UNUSED(ewsItem) + + return true; +} + +EwsModifyItemJob *EwsContactHandler::modifyItemJob(EwsClient &client, const QVector &items, + const QSet &parts, QObject *parent) +{ + return new EwsModifyContactJob(client, items, parts, parent); +} + +EwsCreateItemJob *EwsContactHandler::createItemJob(EwsClient &client, const Akonadi::Item &item, + const Akonadi::Collection &collection, + EwsTagStore *tagStore, EwsResource *parent) +{ + return new EwsCreateContactJob(client, item, collection, tagStore, parent); +} + +EWS_DECLARE_ITEM_HANDLER(EwsContactHandler, EwsItemTypeContact) diff --git a/resources/ews/contact/ewscreatecontactjob.h b/resources/ews/contact/ewscreatecontactjob.h new file mode 100644 --- /dev/null +++ b/resources/ews/contact/ewscreatecontactjob.h @@ -0,0 +1,37 @@ +/* + 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. +*/ + +#ifndef EWSCREATECONTACTJOB_H +#define EWSCREATECONTACTJOB_H + +#include "ewscreateitemjob.h" + +class EwsCreateContactJob : public EwsCreateItemJob +{ + Q_OBJECT +public: + EwsCreateContactJob(EwsClient &client, const Akonadi::Item &item, + const Akonadi::Collection &collection, EwsTagStore *tagStore, EwsResource *parent); + virtual ~EwsCreateContactJob(); + virtual bool setSend(bool send = true) override; +protected: + virtual void doStart() override; +}; + +#endif diff --git a/resources/ews/contact/ewscreatecontactjob.cpp b/resources/ews/contact/ewscreatecontactjob.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/contact/ewscreatecontactjob.cpp @@ -0,0 +1,46 @@ +/* + 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 "ewscreatecontactjob.h" + +#include "ewsresource_debug.h" + +EwsCreateContactJob::EwsCreateContactJob(EwsClient &client, const Akonadi::Item &item, + const Akonadi::Collection &collection, + EwsTagStore *tagStore, EwsResource *parent) + : EwsCreateItemJob(client, item, collection, tagStore, parent) +{ +} +EwsCreateContactJob::~EwsCreateContactJob() +{ +} + +void EwsCreateContactJob::doStart() +{ + qCWarning(EWSRES_LOG) << QStringLiteral("Contact item creation not implemented!"); + emitResult(); +} + +bool EwsCreateContactJob::setSend(bool send) +{ + Q_UNUSED(send) + + qCWarning(EWSRES_LOG) << QStringLiteral("Sending contact items is not supported!"); + return false; +} diff --git a/resources/ews/contact/ewsfetchcontactdetailjob.h b/resources/ews/contact/ewsfetchcontactdetailjob.h new file mode 100644 --- /dev/null +++ b/resources/ews/contact/ewsfetchcontactdetailjob.h @@ -0,0 +1,35 @@ +/* + 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. +*/ + +#ifndef EWSFETCHCONTACTDETAILJOB_H +#define EWSFETCHCONTACTDETAILJOB_H + +#include "ewsfetchitemdetailjob.h" + +class EwsFetchContactDetailJob : public EwsFetchItemDetailJob +{ + Q_OBJECT +public: + EwsFetchContactDetailJob(EwsClient &client, QObject *parent, const Akonadi::Collection &collection); + virtual ~EwsFetchContactDetailJob(); +protected: + virtual void processItems(const QList &responses) override; +}; + +#endif diff --git a/resources/ews/contact/ewsfetchcontactdetailjob.cpp b/resources/ews/contact/ewsfetchcontactdetailjob.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/contact/ewsfetchcontactdetailjob.cpp @@ -0,0 +1,60 @@ +/* + 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 "ewsfetchcontactdetailjob.h" +#include "ewsitemshape.h" +#include "ewsgetitemrequest.h" +#include "ewsmailbox.h" +#include "ewsresource_debug.h" + +using namespace Akonadi; + +EwsFetchContactDetailJob::EwsFetchContactDetailJob(EwsClient &client, QObject *parent, const Akonadi::Collection &collection) + : EwsFetchItemDetailJob(client, parent, collection) +{ + EwsItemShape shape(EwsShapeIdOnly); + mRequest->setItemShape(shape); +} + + +EwsFetchContactDetailJob::~EwsFetchContactDetailJob() +{ +} + +void EwsFetchContactDetailJob::processItems(const QList &responses) +{ + Item::List::iterator it = mChangedItems.begin(); + + Q_FOREACH (const EwsGetItemRequest::Response &resp, responses) { + Item &item = *it; + + if (!resp.isSuccess()) { + qCWarningNC(EWSRES_LOG) << QStringLiteral("Failed to fetch item %1").arg(item.remoteId()); + continue; + } + + //const EwsItem &ewsItem = resp.item(); + + // TODO: Implement + + ++it; + } + + emitResult(); +} diff --git a/resources/ews/contact/ewsmodifycontactjob.h b/resources/ews/contact/ewsmodifycontactjob.h new file mode 100644 --- /dev/null +++ b/resources/ews/contact/ewsmodifycontactjob.h @@ -0,0 +1,35 @@ +/* + 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. +*/ + +#ifndef EWSMODIFYCONTACTJOB_H +#define EWSMODIFYCONTACTJOB_H + +#include "ewsmodifyitemjob.h" + +class EwsModifyContactJob : public EwsModifyItemJob +{ + Q_OBJECT +public: + EwsModifyContactJob(EwsClient &client, const Akonadi::Item::List &items, const QSet &parts, + QObject *parent); + virtual ~EwsModifyContactJob(); + virtual void start() override; +}; + +#endif diff --git a/resources/ews/contact/ewsmodifycontactjob.cpp b/resources/ews/contact/ewsmodifycontactjob.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/contact/ewsmodifycontactjob.cpp @@ -0,0 +1,37 @@ +/* + 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 "ewsmodifycontactjob.h" + +#include "ewsresource_debug.h" + +EwsModifyContactJob::EwsModifyContactJob(EwsClient &client, const Akonadi::Item::List &items, + const QSet &parts, QObject *parent) + : EwsModifyItemJob(client, items, parts, parent) +{ +} +EwsModifyContactJob::~EwsModifyContactJob() +{ +} + +void EwsModifyContactJob::start() +{ + qCWarning(EWSRES_LOG) << QStringLiteral("Contact item modification not implemented!"); + emitResult(); +} diff --git a/resources/ews/ewsautodiscoveryjob.h b/resources/ews/ewsautodiscoveryjob.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsautodiscoveryjob.h @@ -0,0 +1,67 @@ +/* + 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. +*/ + +#ifndef EWSAUTODISCOVERYJOB_H +#define EWSAUTODISCOVERYJOB_H + +#include + +#include "ewsjob.h" + +class EwsAutodiscoveryJob : public EwsJob +{ + Q_OBJECT +public: + EwsAutodiscoveryJob(const QString &email, const QString &username, const QString &password, const QString &userAgent, + bool enableNTLMv2, QObject *parent); + virtual ~EwsAutodiscoveryJob(); + + virtual void start() override; + + const QString &ewsUrl() const + { + return mEwsUrl; + }; + const QString &oabUrl() const + { + return mOabUrl; + }; + +private Q_SLOTS: + void autodiscoveryRequestFinished(KJob *job); +private: + void addUrls(const QString &domain); + void sendNextRequest(bool useCreds); + void parseEmail(); + + QString mEmail; + QString mUsername; + QString mPassword; + + QString mUserAgent; + bool mEnableNTLMv2; + + QQueue mUrlQueue; + + QString mEwsUrl; + QString mOabUrl; + bool mUsedCreds; +}; + +#endif diff --git a/resources/ews/ewsautodiscoveryjob.cpp b/resources/ews/ewsautodiscoveryjob.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsautodiscoveryjob.cpp @@ -0,0 +1,149 @@ +/* + 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 "ewsautodiscoveryjob.h" + +#include +#include "ewspoxautodiscoverrequest.h" +#include "ewsresource_debug.h" + +EwsAutodiscoveryJob::EwsAutodiscoveryJob(const QString &email, const QString &username, const QString &password, + const QString &userAgent, bool enableNTLMv2, QObject *parent) + : EwsJob(parent), mEmail(email), mUsername(username), mPassword(password), mUserAgent(userAgent), + mEnableNTLMv2(enableNTLMv2), mUsedCreds(false) +{ +} + +EwsAutodiscoveryJob::~EwsAutodiscoveryJob() +{ +} + +void EwsAutodiscoveryJob::start() +{ + parseEmail(); + + if (!mUrlQueue.isEmpty()) { + sendNextRequest(false); + } +} + +void EwsAutodiscoveryJob::parseEmail() +{ + int atIndex = mEmail.indexOf(QChar::fromLatin1('@')); + if (atIndex < 0) { + setErrorMsg(i18n("Incorrect email address")); + emitResult(); + return; + } + + QString domain = mEmail.mid(atIndex + 1); + if (domain.isEmpty()) { + setErrorMsg(i18n("Incorrect email address")); + emitResult(); + return; + } + + addUrls(domain); +} + +void EwsAutodiscoveryJob::addUrls(const QString &domain) +{ + mUrlQueue.enqueue(QStringLiteral("https://") + domain + QStringLiteral("/autodiscover/autodiscover.xml")); + mUrlQueue.enqueue(QStringLiteral("https://autodiscover.") + domain + QStringLiteral("/autodiscover/autodiscover.xml")); + mUrlQueue.enqueue(QStringLiteral("http://") + domain + QStringLiteral("/autodiscover/autodiscover.xml")); + mUrlQueue.enqueue(QStringLiteral("http://autodiscover.") + domain + QStringLiteral("/autodiscover/autodiscover.xml")); +} + +void EwsAutodiscoveryJob::sendNextRequest(bool useCreds) +{ + QUrl url(mUrlQueue.head()); + if (useCreds) { + url.setUserName(mUsername); + url.setPassword(mPassword); + } + mUsedCreds = useCreds; + EwsPoxAutodiscoverRequest *req = new EwsPoxAutodiscoverRequest(url, mEmail, mUserAgent, + mEnableNTLMv2, this); + connect(req, &EwsPoxAutodiscoverRequest::result, this, + &EwsAutodiscoveryJob::autodiscoveryRequestFinished); + req->start(); +} + +void EwsAutodiscoveryJob::autodiscoveryRequestFinished(KJob *job) +{ + EwsPoxAutodiscoverRequest *req = qobject_cast(job); + if (!req) { + setErrorMsg(QStringLiteral("Invalid EwsPoxAutodiscoverRequest job object")); + emitResult(); + } + + if (req->error()) { + + if (req->error() == 401 && !mUsedCreds && + req->lastHttpUrl().scheme() != QStringLiteral("http")) { // Don't try authentication over HTTP + + /* The 401 error may have come from an URL different to the original one (due to + * redirections). When the original URL is retried with credentials KIO HTTP will issue + * a warning that an authenticated request is made to a host that never asked for it. + * To fix this restart the request using the last URL that resulted in the 401 code. */ + mUrlQueue.head() = req->lastHttpUrl().toString(); + sendNextRequest(true); + return; + } else { + mUrlQueue.removeFirst(); + } + + if (mUrlQueue.isEmpty()) { + setErrorText(job->errorText()); + setError(job->error()); + emitResult(); + } else { + sendNextRequest(false); + } + } else { + switch (req->action()) { + case EwsPoxAutodiscoverRequest::Settings: { + EwsPoxAutodiscoverRequest::Protocol proto = req->protocol(EwsPoxAutodiscoverRequest::ExchangeProto); + if (!proto.isValid()) { + setErrorMsg(i18n("Exchange protocol information not found")); + } else { + mEwsUrl = proto.ewsUrl(); + mOabUrl = proto.oabUrl(); + } + emitResult(); + break; + } + case EwsPoxAutodiscoverRequest::RedirectAddr: + qCDebug(EWSRES_LOG) << "Redirected to e-mail addr" << req->redirectAddr(); + mEmail = req->redirectAddr(); + mUrlQueue.clear(); + parseEmail(); + if (!mUrlQueue.isEmpty()) { + sendNextRequest(false); + } + break; + case EwsPoxAutodiscoverRequest::RedirectUrl: + qCDebug(EWSRES_LOG) << "Redirected to URL" << req->redirectUrl(); + mUrlQueue.clear(); + mUrlQueue.enqueue(req->redirectUrl()); + sendNextRequest(false); + break; + } + } +} diff --git a/resources/ews/ewsclient/CMakeLists.txt b/resources/ews/ewsclient/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/CMakeLists.txt @@ -0,0 +1,92 @@ +# +# 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_directories(${CMAKE_CURRENT_BINARY_DIR}/../) + +set(EWSCLIENT_SRCS + ewsattachment.cpp + ewsattendee.cpp + ewsclient.cpp + ewscreatefolderrequest.cpp + ewscreateitemrequest.cpp + ewsdeletefolderrequest.cpp + ewsdeleteitemrequest.cpp + ewseffectiverights.cpp + ewseventrequestbase.cpp + ewsfindfolderrequest.cpp + ewsfinditemrequest.cpp + ewsfolder.cpp + ewsfoldershape.cpp + ewsgeteventsrequest.cpp + ewsgetstreamingeventsrequest.cpp + ewsgetfolderrequest.cpp + ewsgetitemrequest.cpp + ewsid.cpp + ewsitem.cpp + ewsitembase.cpp + ewsitemshape.cpp + ewsjob.cpp + ewsmailbox.cpp + ewsmovefolderrequest.cpp + ewsmoveitemrequest.cpp + ewsoccurrence.cpp + ewspoxautodiscoverrequest.cpp + ewspropertyfield.cpp + ewsrecurrence.cpp + ewsrequest.cpp + ewsserverversion.cpp + ewssubscriberequest.cpp + ewssyncfolderhierarchyrequest.cpp + ewssyncfolderitemsrequest.cpp + ewstypes.cpp + ewsunsubscriberequest.cpp + ewsupdatefolderrequest.cpp + ewsupdateitemrequest.cpp + ewsxml.cpp + ewsclient_debug.cpp) + +ecm_qt_declare_logging_category(EWSCLIENT_SRCS + HEADER ewscli_debug.h + IDENTIFIER EWSCLI_LOG + CATEGORY_NAME org.kde.pim.ews.client) +ecm_qt_declare_logging_category(EWSCLIENT_SRCS + HEADER ewscli_proto_debug.h + IDENTIFIER EWSCLI_PROTO_LOG + CATEGORY_NAME org.kde.pim.ews.client.proto + DEFAULT_SEVERITY Warning) +ecm_qt_declare_logging_category(EWSCLIENT_SRCS + HEADER ewscli_req_debug.h + IDENTIFIER EWSCLI_REQUEST_LOG + CATEGORY_NAME org.kde.pim.ews.client.request + DEFAULT_SEVERITY Warning) +ecm_qt_declare_logging_category(EWSCLIENT_SRCS + HEADER ewscli_failedreq_debug.h + IDENTIFIER EWSCLI_FAILEDREQUEST_LOG + CATEGORY_NAME org.kde.pim.ews.client.failedrequest + DEFAULT_SEVERITY Warning) + +add_library(ewsclient STATIC ${EWSCLIENT_SRCS}) +target_link_libraries(ewsclient + Qt5::Network + KF5::KIOCore + KF5::KIOFileWidgets + KF5::KIOWidgets + KF5::KIONTLM + KF5::Codecs + KF5::Mime + KF5::CalendarCore) diff --git a/resources/ews/ewsclient/ewsattachment.h b/resources/ews/ewsclient/ewsattachment.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsattachment.h @@ -0,0 +1,123 @@ +/* + 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 EWSATTACHMENT_H +#define EWSATTACHMENT_H + +#include +#include +#include +#include +#include + +#include "ewsitem.h" +#include "ewspropertyfield.h" +#include "ewstypes.h" + +class EwsAttachmentPrivate; + +class EwsAttachment +{ +public: + enum Type { + UnknownAttachment, + ItemAttachment, + FileAttachment, + ReferenceAttachment // Occurs in 2016 XSD, but not documented on MSDN + }; + + typedef QVector List; + + EwsAttachment(); + ~EwsAttachment(); + explicit EwsAttachment(QXmlStreamReader &reader); + EwsAttachment(const EwsAttachment &other); + EwsAttachment(EwsAttachment &&other); + EwsAttachment &operator=(EwsAttachment &&other); + EwsAttachment &operator=(const EwsAttachment &other); + + bool isValid() const; + + Type type() const; + void setType(Type type); + + QString id() const; + void setId(const QString &id); + void resetId(); + bool hasId() const; + + QString name() const; + void setName(const QString &name); + void resetName(); + bool hasName() const; + + QString contentType() const; + void setContentType(const QString &contentType); + void resetContentType(); + bool hasContentType() const; + + QString contentId() const; + void setContentId(const QString &contentId); + void resetContentId(); + bool hasContentId() const; + + QString contentLocation() const; + void setContentLocation(const QString &contentLocation); + void resetContentLocation(); + bool hasContentLocation() const; + + long size() const; + void setSize(long size); + void resetSize(); + bool hasSize() const; + + QDateTime lastModifiedTime() const; + void setLastModifiedTime(const QDateTime &time); + void resetLastModifiedTime(); + bool hasLastModifiedTime() const; + + bool isInline() const; + void setIsInline(bool isInline); + void resetIsInline(); + bool hasIsInline() const; + + bool isContactPhoto() const; + void setIsContactPhoto(bool isContactPhoto); + void resetIsContactPhoto(); + bool hasIsContactPhoto() const; + + QByteArray content() const; + void setContent(const QByteArray &content); + void resetContent(); + bool hasContent() const; + + const EwsItem &item() const; + void setItem(const EwsItem &item); + void resetItem(); + bool hasItem() const; + + void write(QXmlStreamWriter &writer) const; +protected: + QSharedDataPointer d; +}; + +Q_DECLARE_METATYPE(EwsAttachment) +Q_DECLARE_METATYPE(EwsAttachment::List) + +#endif diff --git a/resources/ews/ewsclient/ewsattachment.cpp b/resources/ews/ewsclient/ewsattachment.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsattachment.cpp @@ -0,0 +1,532 @@ +/* + 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 "ewsattachment.h" + +#include + +#include "ewsclient_debug.h" +#include "ewsxml.h" + +class EwsAttachmentPrivate : public QSharedData +{ +public: + EwsAttachmentPrivate(); + ~EwsAttachmentPrivate(); + + enum Field { + Id = 0, + Name, + ContentType, + ContentId, + ContentLocation, + Size, + LastModifiedTime, + IsInline, + IsContactPhoto, + Content, + Item, + NumFields + }; + + + EwsAttachment::Type mType; + QString mId; + QString mName; + QString mContentType; + QString mContentId; + QString mContentLocation; + long mSize; + QDateTime mLastModifiedTime; + bool mIsInline; + bool mIsContactPhoto; + QByteArray mContent; + EwsItem mItem; + bool mValid; + QBitArray mValidFields; +}; + +EwsAttachmentPrivate::EwsAttachmentPrivate() + : mType(EwsAttachment::UnknownAttachment), mSize(0), mIsInline(false), mIsContactPhoto(false), mValid(false), + mValidFields(NumFields) +{ +} + +EwsAttachmentPrivate::~EwsAttachmentPrivate() +{ +} + +EwsAttachment::EwsAttachment() + : d(new EwsAttachmentPrivate()) +{ +} + +EwsAttachment::~EwsAttachment() +{ +} + +EwsAttachment::EwsAttachment(QXmlStreamReader &reader) + : d(new EwsAttachmentPrivate()) +{ + bool ok = true; + + if (reader.namespaceUri() != ewsTypeNsUri) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected namespace in Attachment element:") + << reader.namespaceUri(); + reader.skipCurrentElement(); + return; + } + + if (reader.name() == QStringLiteral("ItemAttachment")) { + d->mType = ItemAttachment; + } else if (reader.name() == QStringLiteral("FileAttachment")) { + d->mType = FileAttachment; + } else if (reader.name() == QStringLiteral("ReferenceAttachment")) { + d->mType = ReferenceAttachment; + } else { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unknown attachment type %1").arg(reader.name().toString()); + ok = false; + } + + // Skip this attachment type as it's not clearly documented. + if (d->mType == ReferenceAttachment) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Attachment type ReferenceAttachment not fully supported"); + reader.skipCurrentElement(); + d->mValid = true; + return; + } + + while (ok && reader.readNextStartElement()) { + if (reader.namespaceUri() != ewsTypeNsUri) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected namespace in Attachment element:") + << reader.namespaceUri(); + reader.skipCurrentElement(); + ok = false; + break; + } + + QString elmName = reader.name().toString(); + if (elmName == QStringLiteral("AttachmentId")) { + QXmlStreamAttributes attrs = reader.attributes(); + if (!attrs.hasAttribute(QStringLiteral("Id"))) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - missing Id in AttachmentId element.") + .arg(QStringLiteral("Attachment")); + reader.skipCurrentElement(); + ok = false; + } else { + d->mId = attrs.value(QStringLiteral("Id")).toString(); + d->mValidFields.setBit(EwsAttachmentPrivate::Id); + } + reader.skipCurrentElement(); + } else if (elmName == QStringLiteral("Name")) { + d->mName = readXmlElementValue(reader, ok, QStringLiteral("Attachment")); + d->mValidFields.setBit(EwsAttachmentPrivate::Name, ok); + } else if (elmName == QStringLiteral("ContentType")) { + d->mContentType = readXmlElementValue(reader, ok, QStringLiteral("Attachment")); + d->mValidFields.setBit(EwsAttachmentPrivate::ContentType, ok); + } else if (elmName == QStringLiteral("ContentId")) { + d->mContentId = readXmlElementValue(reader, ok, QStringLiteral("Attachment")); + d->mValidFields.setBit(EwsAttachmentPrivate::ContentId, ok); + } else if (elmName == QStringLiteral("ContentLocation")) { + d->mContentLocation = readXmlElementValue(reader, ok, QStringLiteral("Attachment")); + d->mValidFields.setBit(EwsAttachmentPrivate::ContentLocation, ok); + } else if (elmName == QStringLiteral("AttachmentOriginalUrl")) { + // Ignore + reader.skipCurrentElement(); + } else if (elmName == QStringLiteral("Size")) { + d->mSize = readXmlElementValue(reader, ok, QStringLiteral("Attachment")); + d->mValidFields.setBit(EwsAttachmentPrivate::Size, ok); + } else if (elmName == QStringLiteral("LastModifiedTime")) { + d->mLastModifiedTime = readXmlElementValue(reader, ok, QStringLiteral("Attachment")); + d->mValidFields.setBit(EwsAttachmentPrivate::LastModifiedTime, ok); + } else if (elmName == QStringLiteral("IsInline")) { + d->mIsInline = readXmlElementValue(reader, ok, QStringLiteral("Attachment")); + d->mValidFields.setBit(EwsAttachmentPrivate::IsInline, ok); + } else if (d->mType == FileAttachment && elmName == QStringLiteral("IsContactPhoto")) { + d->mIsContactPhoto = readXmlElementValue(reader, ok, QStringLiteral("Attachment")); + d->mValidFields.setBit(EwsAttachmentPrivate::IsContactPhoto, ok); + } else if (d->mType == FileAttachment && elmName == QStringLiteral("Content")) { + d->mContent = readXmlElementValue(reader, ok, QStringLiteral("Attachment")); + d->mValidFields.setBit(EwsAttachmentPrivate::Content, ok); + } else if (d->mType == ItemAttachment && (elmName == QStringLiteral("Item") || elmName == QStringLiteral("Message") || + elmName == QStringLiteral("CalendarItem") || elmName == QStringLiteral("Contact") || + elmName == QStringLiteral("MeetingMessage") || elmName == QStringLiteral("MeetingRequest") || + elmName == QStringLiteral("MeetingResponse") || elmName == QStringLiteral("MeetingCancellation") || + elmName == QStringLiteral("Task"))) { + d->mItem = EwsItem(reader); + if (!d->mItem.isValid()) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid %2 element.") + .arg(QStringLiteral("Attachment")).arg(QStringLiteral("Item")); + reader.skipCurrentElement(); + ok = false; + } else { + d->mValidFields.setBit(EwsAttachmentPrivate::Item); + } + } else { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - unknown %2 element.") + .arg(QStringLiteral("Attachment")).arg(elmName); + reader.skipCurrentElement(); + ok = false; + } + } + + if (!ok) { + reader.skipCurrentElement(); + } + d->mValid = ok; +} + +EwsAttachment::EwsAttachment(const EwsAttachment &other) +{ + d = other.d; +} + +EwsAttachment::EwsAttachment(EwsAttachment &&other) +{ + d = std::move(other.d); +} + +EwsAttachment &EwsAttachment::operator=(EwsAttachment &&other) +{ + d = std::move(other.d); + return *this; +} + +EwsAttachment &EwsAttachment::operator=(const EwsAttachment &other) +{ + d = other.d; + return *this; +} + +void EwsAttachment::write(QXmlStreamWriter &writer) const +{ + QString elmName; + switch (d->mType) { + case ItemAttachment: + elmName = QStringLiteral("ItemAttachment"); + break; + case FileAttachment: + elmName = QStringLiteral("FileAttachment"); + break; + case ReferenceAttachment: + elmName = QStringLiteral("ReferenceAttachment"); + break; + default: + qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to write Attachment element - invalid attachment type."); + return; + } + writer.writeStartElement(ewsTypeNsUri, elmName); + + if (d->mType == ReferenceAttachment) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Attachment type ReferenceAttachment not fully supported"); + writer.writeEndElement(); + return; + } + + if (d->mValidFields[EwsAttachmentPrivate::Id]) { + writer.writeStartElement(ewsTypeNsUri, QStringLiteral("AttachmentId")); + writer.writeAttribute(QStringLiteral("Id"), d->mId); + writer.writeEndElement(); + } + if (d->mValidFields[EwsAttachmentPrivate::Name]) { + writer.writeTextElement(ewsTypeNsUri, QStringLiteral("Name"), d->mName); + } + if (d->mValidFields[EwsAttachmentPrivate::ContentType]) { + writer.writeTextElement(ewsTypeNsUri, QStringLiteral("ContentType"), d->mContentType); + } + if (d->mValidFields[EwsAttachmentPrivate::ContentId]) { + writer.writeTextElement(ewsTypeNsUri, QStringLiteral("ContentId"), d->mContentId); + } + if (d->mValidFields[EwsAttachmentPrivate::ContentLocation]) { + writer.writeTextElement(ewsTypeNsUri, QStringLiteral("ContentLocation"), d->mContentLocation); + } + if (d->mValidFields[EwsAttachmentPrivate::Size]) { + writer.writeTextElement(ewsTypeNsUri, QStringLiteral("Size"), QString::number(d->mSize)); + } + if (d->mValidFields[EwsAttachmentPrivate::LastModifiedTime]) { + writer.writeTextElement(ewsTypeNsUri, QStringLiteral("LastModifiedTime"), d->mLastModifiedTime.toString(Qt::ISODate)); + } + if (d->mValidFields[EwsAttachmentPrivate::IsInline]) { + writer.writeTextElement(ewsTypeNsUri, QStringLiteral("IsInline"), + d->mIsInline ? QStringLiteral("true") : QStringLiteral("false")); + } + if (d->mType == FileAttachment) { + if (d->mValidFields[EwsAttachmentPrivate::IsContactPhoto]) { + writer.writeTextElement(ewsTypeNsUri, QStringLiteral("IsContactPhoto"), + d->mIsContactPhoto ? QStringLiteral("true") : QStringLiteral("false")); + } + if (d->mValidFields[EwsAttachmentPrivate::Content]) { + writer.writeTextElement(ewsTypeNsUri, QStringLiteral("Content"), QString::fromLatin1(d->mContent.toBase64())); + } + } else if (d->mType == ItemAttachment) { + if (d->mValidFields[EwsAttachmentPrivate::Item]) { + d->mItem.write(writer); + } + } + + writer.writeEndElement(); +} + +bool EwsAttachment::isValid() const +{ + return d->mValid; +} + +EwsAttachment::Type EwsAttachment::type() const +{ + return d->mType; +} + +void EwsAttachment::setType(Type type) +{ + d->mType = type; +} + +QString EwsAttachment::id() const +{ + return d->mId; +} + +void EwsAttachment::setId(const QString &id) +{ + d->mId = id; + d->mValidFields.setBit(EwsAttachmentPrivate::Id); +} + +void EwsAttachment::resetId() +{ + d->mValidFields.clearBit(EwsAttachmentPrivate::Id); +} + +bool EwsAttachment::hasId() const +{ + return d->mValidFields[EwsAttachmentPrivate::Id]; +} + +QString EwsAttachment::name() const +{ + return d->mName; +} + +void EwsAttachment::setName(const QString &name) +{ + d->mName = name; + d->mValidFields.setBit(EwsAttachmentPrivate::Name); +} + +void EwsAttachment::resetName() +{ + d->mValidFields.clearBit(EwsAttachmentPrivate::Name); +} + +bool EwsAttachment::hasName() const +{ + return d->mValidFields[EwsAttachmentPrivate::Name]; +} + +QString EwsAttachment::contentType() const +{ + return d->mContentType; +} + +void EwsAttachment::setContentType(const QString &contentType) +{ + d->mContentType = contentType; + d->mValidFields.setBit(EwsAttachmentPrivate::ContentType); +} + +void EwsAttachment::resetContentType() +{ + d->mValidFields.clearBit(EwsAttachmentPrivate::ContentType); +} + +bool EwsAttachment::hasContentType() const +{ + return d->mValidFields[EwsAttachmentPrivate::ContentType]; +} + +QString EwsAttachment::contentId() const +{ + return d->mContentId; +} + +void EwsAttachment::setContentId(const QString &contentId) +{ + d->mContentId = contentId; + d->mValidFields.setBit(EwsAttachmentPrivate::ContentId); +} + +void EwsAttachment::resetContentId() +{ + d->mValidFields.clearBit(EwsAttachmentPrivate::ContentId); +} + +bool EwsAttachment::hasContentId() const +{ + return d->mValidFields[EwsAttachmentPrivate::ContentId]; +} + +QString EwsAttachment::contentLocation() const +{ + return d->mContentLocation; +} + +void EwsAttachment::setContentLocation(const QString &contentLocation) +{ + d->mContentLocation = contentLocation; + d->mValidFields.setBit(EwsAttachmentPrivate::ContentLocation); +} + +void EwsAttachment::resetContentLocation() +{ + d->mValidFields.clearBit(EwsAttachmentPrivate::ContentLocation); +} + +bool EwsAttachment::hasContentLocation() const +{ + return d->mValidFields[EwsAttachmentPrivate::ContentLocation]; +} + +long EwsAttachment::size() const +{ + return d->mSize; +} + +void EwsAttachment::setSize(long size) +{ + d->mSize = size; + d->mValidFields.setBit(EwsAttachmentPrivate::Size); +} + +void EwsAttachment::resetSize() +{ + d->mValidFields.clearBit(EwsAttachmentPrivate::Size); +} + +bool EwsAttachment::hasSize() const +{ + return d->mValidFields[EwsAttachmentPrivate::Size]; +} + +QDateTime EwsAttachment::lastModifiedTime() const +{ + return d->mLastModifiedTime; +} + +void EwsAttachment::setLastModifiedTime(const QDateTime &time) +{ + d->mLastModifiedTime = time; + d->mValidFields.setBit(EwsAttachmentPrivate::LastModifiedTime); +} + +void EwsAttachment::resetLastModifiedTime() +{ + d->mValidFields.clearBit(EwsAttachmentPrivate::LastModifiedTime); +} + +bool EwsAttachment::hasLastModifiedTime() const +{ + return d->mValidFields[EwsAttachmentPrivate::LastModifiedTime]; +} + +bool EwsAttachment::isInline() const +{ + return d->mIsInline; +} + +void EwsAttachment::setIsInline(bool isInline) +{ + d->mIsInline = isInline; + d->mValidFields.setBit(EwsAttachmentPrivate::IsInline); +} + +void EwsAttachment::resetIsInline() +{ + d->mValidFields.clearBit(EwsAttachmentPrivate::IsInline); +} + +bool EwsAttachment::hasIsInline() const +{ + return d->mValidFields[EwsAttachmentPrivate::IsInline]; +} + +bool EwsAttachment::isContactPhoto() const +{ + return d->mIsContactPhoto; +} + +void EwsAttachment::setIsContactPhoto(bool isContactPhoto) +{ + d->mIsContactPhoto = isContactPhoto; + d->mValidFields.setBit(EwsAttachmentPrivate::IsContactPhoto); +} + +void EwsAttachment::resetIsContactPhoto() +{ + d->mValidFields.clearBit(EwsAttachmentPrivate::IsContactPhoto); +} + +bool EwsAttachment::hasIsContactPhoto() const +{ + return d->mValidFields[EwsAttachmentPrivate::IsContactPhoto]; +} + +QByteArray EwsAttachment::content() const +{ + return d->mContent; +} + +void EwsAttachment::setContent(const QByteArray &content) +{ + d->mContent = content; + d->mValidFields.setBit(EwsAttachmentPrivate::Content); +} + +void EwsAttachment::resetContent() +{ + d->mValidFields.clearBit(EwsAttachmentPrivate::Content); +} + +bool EwsAttachment::hasContent() const +{ + return d->mValidFields[EwsAttachmentPrivate::Content]; +} + +const EwsItem &EwsAttachment::item() const +{ + return d->mItem; +} + +void EwsAttachment::setItem(const EwsItem &item) +{ + d->mItem = item; + d->mValidFields.setBit(EwsAttachmentPrivate::Item); +} + +void EwsAttachment::resetItem() +{ + d->mValidFields.clearBit(EwsAttachmentPrivate::Item); +} + +bool EwsAttachment::hasItem() const +{ + return d->mValidFields[EwsAttachmentPrivate::Item]; +} + + diff --git a/resources/ews/ewsclient/ewsattendee.h b/resources/ews/ewsclient/ewsattendee.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsattendee.h @@ -0,0 +1,58 @@ +/* + 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. +*/ + +#ifndef EWSATTENDEE_H +#define EWSATTENDEE_H + +#include +#include +#include + +#include "ewstypes.h" + +class EwsAttendeePrivate; +class EwsMailbox; +class QXmlStreamReader; + +class EwsAttendee +{ +public: + typedef QList List; + + EwsAttendee(); + explicit EwsAttendee(QXmlStreamReader &reader); + EwsAttendee(const EwsAttendee &other); + EwsAttendee(EwsAttendee &&other); + virtual ~EwsAttendee(); + + EwsAttendee &operator=(const EwsAttendee &other); + EwsAttendee &operator=(EwsAttendee &&other); + + bool isValid() const; + const EwsMailbox &mailbox() const; + EwsEventResponseType response() const; + QDateTime responseTime() const; +protected: + QSharedDataPointer d; +}; + +Q_DECLARE_METATYPE(EwsAttendee) +Q_DECLARE_METATYPE(EwsAttendee::List) + +#endif diff --git a/resources/ews/ewsclient/ewsattendee.cpp b/resources/ews/ewsclient/ewsattendee.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsattendee.cpp @@ -0,0 +1,145 @@ +/* + 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 "ewsattendee.h" + +#include +#include + +#include "ewsclient_debug.h" +#include "ewsmailbox.h" +#include "ewstypes.h" + +class EwsAttendeePrivate : public QSharedData +{ +public: + EwsAttendeePrivate(); + virtual ~EwsAttendeePrivate(); + + bool mValid; + + EwsMailbox mMailbox; + EwsEventResponseType mResponse; + QDateTime mResponseTime; +}; + +static const QString responseTypeNames[] = { + QStringLiteral("Unknown"), + QStringLiteral("Organizer"), + QStringLiteral("Tentative"), + QStringLiteral("Accept"), + QStringLiteral("Decline"), + QStringLiteral("NoResponseReceived") +}; +Q_CONSTEXPR unsigned responseTypeNameCount = sizeof(responseTypeNames) / sizeof(responseTypeNames[0]); + +EwsAttendeePrivate::EwsAttendeePrivate() + : mValid(false), mResponse(EwsEventResponseNotReceived) +{ +} + +EwsAttendee::EwsAttendee() + : d(new EwsAttendeePrivate()) +{ +} + +EwsAttendee::EwsAttendee(QXmlStreamReader &reader) + : d(new EwsAttendeePrivate()) +{ + while (reader.readNextStartElement()) { + if (reader.namespaceUri() != ewsTypeNsUri) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected namespace in mailbox element:") + << reader.namespaceUri(); + return; + } + + if (reader.name() == QStringLiteral("Mailbox")) { + d->mMailbox = EwsMailbox(reader); + if (!d->mMailbox.isValid()) { + qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read EWS request - invalid attendee %1 element.") + .arg(reader.name().toString()); + return; + } + } else if (reader.name() == QStringLiteral("ResponseType")) { + bool ok; + d->mResponse = decodeEnumString(reader.readElementText(), + responseTypeNames, responseTypeNameCount, &ok); + if (reader.error() != QXmlStreamReader::NoError || !ok) { + qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read EWS request - invalid attendee %1 element.") + .arg(reader.name().toString()); + return; + } + } else if (reader.name() == QStringLiteral("LastResponseTime")) { + // Unsupported - ignore + //qCWarningNC(EWSCLIENT_LOG) << QStringLiteral("Unsupported mailbox element %1").arg(reader.name().toString()); + reader.skipCurrentElement(); + } + } + + d->mValid = true; +} + +EwsAttendeePrivate::~EwsAttendeePrivate() +{ +} + +EwsAttendee::EwsAttendee(const EwsAttendee &other) + : d(other.d) +{ +} + +EwsAttendee::EwsAttendee(EwsAttendee &&other) + : d(std::move(other.d)) +{ +} + +EwsAttendee::~EwsAttendee() +{ +} + +EwsAttendee &EwsAttendee::operator=(const EwsAttendee &other) +{ + d = other.d; + return *this; +} +EwsAttendee &EwsAttendee::operator=(EwsAttendee &&other) +{ + d = std::move(other.d); + return *this; +} + +bool EwsAttendee::isValid() const +{ + return d->mValid; +} + +const EwsMailbox &EwsAttendee::mailbox() const +{ + return d->mMailbox; +} + +EwsEventResponseType EwsAttendee::response() const +{ + return d->mResponse; +} + +QDateTime EwsAttendee::responseTime() const +{ + return d->mResponseTime; +} diff --git a/resources/ews/ewsclient/ewsclient.h b/resources/ews/ewsclient/ewsclient.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsclient.h @@ -0,0 +1,102 @@ +/* + 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. +*/ + +#ifndef EWSCLIENT_H +#define EWSCLIENT_H + +#include +#include +#include +#include + +#include "ewsserverversion.h" + +class EwsClient : public QObject +{ + Q_OBJECT +public: + explicit EwsClient(QObject *parent = nullptr); + ~EwsClient(); + + void setUrl(const QString &url) + { + mUrl.setUrl(url); + } + + void setCredentials(const QString &username, const QString &password) + { + mUrl.setUserName(username); + mUrl.setPassword(password); + } + + enum RequestedConfiguration { + MailTips = 0, + UnifiedMessagingConfiguration, + ProtectionRules + }; + + QUrl url() const + { + return mUrl; + } + + bool isConfigured() const + { + return !mUrl.isEmpty(); + } + + void setServerVersion(const EwsServerVersion &version); + const EwsServerVersion &serverVersion() const + { + return mServerVersion; + }; + + void setUserAgent(const QString &userAgent) + { + mUserAgent = userAgent; + }; + const QString &userAgent() const + { + return mUserAgent; + }; + + void setEnableNTLMv2(bool enable) + { + mEnableNTLMv2 = enable; + }; + bool isNTLMv2Enabled() const + { + return mEnableNTLMv2; + }; + + static QHash folderHash; +private: + QUrl mUrl; + QString mUsername; + QString mPassword; + + QString mUserAgent; + bool mEnableNTLMv2; + + EwsServerVersion mServerVersion; + + friend class EwsRequest; +}; + +#endif diff --git a/resources/ews/ewsclient/ewsclient.cpp b/resources/ews/ewsclient/ewsclient.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsclient.cpp @@ -0,0 +1,44 @@ +/* + 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 "ewsclient.h" + +#include + +#include "ewsclient_debug.h" + +QHash EwsClient::folderHash; + +EwsClient::EwsClient(QObject *parent) + : QObject(parent), mEnableNTLMv2(true) +{ + +} + +EwsClient::~EwsClient() +{ +} + +void EwsClient::setServerVersion(const EwsServerVersion &version) +{ + if (mServerVersion.isValid() && mServerVersion != version) { + qCWarning(EWSCLI_LOG) << "Warning - server version changed." << mServerVersion << version; + } + mServerVersion = version; +} diff --git a/resources/ews/ewsclient/ewsclient_debug.h b/resources/ews/ewsclient/ewsclient_debug.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsclient_debug.h @@ -0,0 +1,52 @@ +/* + 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 EWSCLIENT_DEBUG_H +#define EWSCLIENT_DEBUG_H + +#include +#include +#include +#include +#include +#include + +#define qCDebugNC(cat) qCDebug(cat).noquote() +#define qCInfoNC(cat) qCInfo(cat).noquote() +#define qCWarningNC(cat) qCWarning(cat).noquote() +#define qCCriticalNC(cat) qCCritical(cat).noquote() + +#define qCDebugNS(cat) qCDebug(cat).nospace() +#define qCInfoNS(cat) qCInfo(cat).nospace() +#define qCWarningNS(cat) qCWarning(cat).nospace() +#define qCCriticalNS(cat) qCCritical(cat).nospace() + +#define qCDebugNCS(cat) qCDebug(cat).noquote().nospace() +#define qCInfoNCS(cat) qCInfo(cat).noquote().nospace() +#define qCWarningNCS(cat) qCWarning(cat).noquote().nospace() +#define qCCriticalNCS(cat) qCCritical(cat).noquote().nospace() + +inline QString ewsHash(const QString &val) +{ + return QString::number(qHash(val), 36); +} + +extern QTemporaryDir ewsLogDir; + +#endif diff --git a/resources/ews/ewsclient/ewsclient_debug.cpp b/resources/ews/ewsclient/ewsclient_debug.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsclient_debug.cpp @@ -0,0 +1,22 @@ +/* + 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 "ewsclient_debug.h" + +QTemporaryDir ewsLogDir(QStringLiteral("/tmp/akonadi-ews-XXXXXXX")); diff --git a/resources/ews/ewsclient/ewscreatefolderrequest.h b/resources/ews/ewsclient/ewscreatefolderrequest.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewscreatefolderrequest.h @@ -0,0 +1,79 @@ +/* + 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 EWSCREATEFOLDERREQUEST_H +#define EWSCREATEFOLDERREQUEST_H + +#include +#include + +#include "ewsfolder.h" +#include "ewsrequest.h" +#include "ewstypes.h" + +class QXmlStreamReader; +class QXmlStreamWriter; + +class EwsCreateFolderRequest : public EwsRequest +{ + Q_OBJECT +public: + class Response : public EwsRequest::Response + { + public: + const EwsId &folderId() const + { + return mId; + }; + protected: + Response(QXmlStreamReader &reader); + + EwsId mId; + + friend class EwsCreateFolderRequest; + }; + + EwsCreateFolderRequest(EwsClient &client, QObject *parent); + virtual ~EwsCreateFolderRequest(); + + void setFolders(const EwsFolder::List &folders) + { + mFolders = folders; + }; + void setParentFolderId(const EwsId &id) + { + mParentFolderId = id; + }; + + virtual void start() override; + + const QList &responses() const + { + return mResponses; + }; +protected: + virtual bool parseResult(QXmlStreamReader &reader) override; + bool parseItemsResponse(QXmlStreamReader &reader); +private: + EwsFolder::List mFolders; + EwsId mParentFolderId; + QList mResponses; +}; + +#endif diff --git a/resources/ews/ewsclient/ewscreatefolderrequest.cpp b/resources/ews/ewsclient/ewscreatefolderrequest.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewscreatefolderrequest.cpp @@ -0,0 +1,121 @@ +/* + 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 "ewscreatefolderrequest.h" + +#include "ewsclient_debug.h" + +EwsCreateFolderRequest::EwsCreateFolderRequest(EwsClient &client, QObject *parent) + : EwsRequest(client, parent) +{ +} + +EwsCreateFolderRequest::~EwsCreateFolderRequest() +{ +} + +void EwsCreateFolderRequest::start() +{ + QString reqString; + QXmlStreamWriter writer(&reqString); + + startSoapDocument(writer); + + writer.writeStartElement(ewsMsgNsUri, QStringLiteral("CreateFolder")); + + writer.writeStartElement(ewsMsgNsUri, QStringLiteral("ParentFolderId")); + mParentFolderId.writeFolderIds(writer); + writer.writeEndElement(); + + writer.writeStartElement(ewsMsgNsUri, QStringLiteral("Folders")); + Q_FOREACH (const EwsFolder &folder, mFolders) { + folder.write(writer); + } + writer.writeEndElement(); + + writer.writeEndElement(); + + endSoapDocument(writer); + + qCDebugNC(EWSCLI_REQUEST_LOG) << QStringLiteral("Starting CreateFolder request (%1 folders, parent %2)") + .arg(mFolders.size()).arg(mParentFolderId.id()); + + qCDebug(EWSCLI_PROTO_LOG) << reqString; + + prepare(reqString); + + doSend(); +} + +bool EwsCreateFolderRequest::parseResult(QXmlStreamReader &reader) +{ + return parseResponseMessage(reader, QStringLiteral("CreateFolder"), + [this](QXmlStreamReader &reader) {return parseItemsResponse(reader);}); +} + +bool EwsCreateFolderRequest::parseItemsResponse(QXmlStreamReader &reader) +{ + Response resp(reader); + if (resp.responseClass() == EwsResponseUnknown) { + return false; + } + + if (EWSCLI_REQUEST_LOG().isDebugEnabled()) { + if (resp.isSuccess()) { + qCDebug(EWSCLI_REQUEST_LOG) << QStringLiteral("Got CreateFolder response - OK"); + } else { + qCDebug(EWSCLI_REQUEST_LOG) << QStringLiteral("Got CreateFolder response - %1") + .arg(resp.responseMessage()); + } + } + mResponses.append(resp); + return true; +} + +EwsCreateFolderRequest::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())); + return; + } + + if (reader.name() == QStringLiteral("Folders")) { + if (reader.readNextStartElement()) { + EwsFolder folder(reader); + if (!folder.isValid()) { + return; + } + mId = folder[EwsFolderFieldFolderId].value(); + + // Finish the Folders element. + reader.skipCurrentElement(); + } + } else if (!readResponseElement(reader)) { + setErrorMsg(QStringLiteral("Failed to read EWS request - invalid response element.")); + return; + } + } +} diff --git a/resources/ews/ewsclient/ewscreateitemrequest.h b/resources/ews/ewsclient/ewscreateitemrequest.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewscreateitemrequest.h @@ -0,0 +1,89 @@ +/* + 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 EWSCREATEITEMREQUEST_H +#define EWSCREATEITEMREQUEST_H + +#include +#include + +#include "ewsitem.h" +#include "ewsrequest.h" +#include "ewstypes.h" + +class QXmlStreamReader; +class QXmlStreamWriter; + +class EwsCreateItemRequest : public EwsRequest +{ + Q_OBJECT +public: + class Response : public EwsRequest::Response + { + public: + const EwsId &itemId() const + { + return mId; + }; + protected: + Response(QXmlStreamReader &reader); + + EwsId mId; + + friend class EwsCreateItemRequest; + }; + + EwsCreateItemRequest(EwsClient &client, QObject *parent); + virtual ~EwsCreateItemRequest(); + + void setItems(const EwsItem::List &items) + { + mItems = items; + }; + void setMessageDisposition(EwsMessageDisposition disp) + { + mMessageDisp = disp; + }; + void setMeetingDisposition(EwsMeetingDisposition disp) + { + mMeetingDisp = disp; + }; + void setSavedFolderId(const EwsId &id) + { + mSavedFolderId = id; + }; + + virtual void start() override; + + const QList &responses() const + { + return mResponses; + }; +protected: + virtual bool parseResult(QXmlStreamReader &reader) override; + bool parseItemsResponse(QXmlStreamReader &reader); +private: + EwsItem::List mItems; + EwsId mSavedFolderId; + EwsMessageDisposition mMessageDisp; + EwsMeetingDisposition mMeetingDisp; + QList mResponses; +}; + +#endif diff --git a/resources/ews/ewsclient/ewscreateitemrequest.cpp b/resources/ews/ewsclient/ewscreateitemrequest.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewscreateitemrequest.cpp @@ -0,0 +1,144 @@ +/* + 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 "ewscreateitemrequest.h" +#include "ewsclient_debug.h" + +static const QVector messageDispositionNames = { + QStringLiteral("SaveOnly"), + QStringLiteral("SendOnly"), + QStringLiteral("SendAndSaveCopy") +}; + +static const QVector meetingDispositionNames = { + QStringLiteral("SendToNone"), + QStringLiteral("SendOnlyToAll"), + QStringLiteral("SendOnlyToChanged"), + QStringLiteral("SendToAllAndSaveCopy"), + QStringLiteral("SendToChangedAndSaveCopy") +}; + +EwsCreateItemRequest::EwsCreateItemRequest(EwsClient &client, QObject *parent) + : EwsRequest(client, parent), mMessageDisp(EwsDispSaveOnly), mMeetingDisp(EwsMeetingDispUnspecified) +{ +} + +EwsCreateItemRequest::~EwsCreateItemRequest() +{ +} + +void EwsCreateItemRequest::start() +{ + QString reqString; + QXmlStreamWriter writer(&reqString); + + startSoapDocument(writer); + + writer.writeStartElement(ewsMsgNsUri, QStringLiteral("CreateItem")); + + writer.writeAttribute(QStringLiteral("MessageDisposition"), + messageDispositionNames[mMessageDisp]); + + if (mMeetingDisp != EwsMeetingDispUnspecified) { + writer.writeAttribute(QStringLiteral("SendMeetingInvitations"), + meetingDispositionNames[mMeetingDisp]); + } + + if (mMessageDisp == EwsDispSaveOnly || mMessageDisp == EwsDispSendAndSaveCopy) { + writer.writeStartElement(ewsMsgNsUri, QStringLiteral("SavedItemFolderId")); + mSavedFolderId.writeFolderIds(writer); + writer.writeEndElement(); + } + + writer.writeStartElement(ewsMsgNsUri, QStringLiteral("Items")); + Q_FOREACH (const EwsItem &item, mItems) { + item.write(writer); + } + writer.writeEndElement(); + + writer.writeEndElement(); + + endSoapDocument(writer); + + qCDebugNC(EWSCLI_REQUEST_LOG) << QStringLiteral("Starting CreateItem request (%1 items, parent %2)") + .arg(mItems.size()).arg(mSavedFolderId.id()); + + qCDebug(EWSCLI_PROTO_LOG) << reqString; + + prepare(reqString); + + doSend(); +} + +bool EwsCreateItemRequest::parseResult(QXmlStreamReader &reader) +{ + return parseResponseMessage(reader, QStringLiteral("CreateItem"), + [this](QXmlStreamReader &reader) {return parseItemsResponse(reader);}); +} + +bool EwsCreateItemRequest::parseItemsResponse(QXmlStreamReader &reader) +{ + Response resp(reader); + if (resp.responseClass() == EwsResponseUnknown) { + return false; + } + + if (EWSCLI_REQUEST_LOG().isDebugEnabled()) { + if (resp.isSuccess()) { + qCDebug(EWSCLI_REQUEST_LOG) << QStringLiteral("Got CreateItem response - OK"); + } else { + qCDebug(EWSCLI_REQUEST_LOG) << QStringLiteral("Got CreateItem response - %1") + .arg(resp.responseMessage()); + } + } + mResponses.append(resp); + return true; +} + +EwsCreateItemRequest::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())); + return; + } + + if (reader.name() == QStringLiteral("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/ewsdeletefolderrequest.h b/resources/ews/ewsclient/ewsdeletefolderrequest.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsdeletefolderrequest.h @@ -0,0 +1,77 @@ +/* + 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 EWSDELETEFOLDERREQUEST_H +#define EWSDELETEFOLDERREQUEST_H + +#include + +#include "ewsfolder.h" +#include "ewsrequest.h" +#include "ewstypes.h" + +class QXmlStreamReader; + +class EwsDeleteFolderRequest : public EwsRequest +{ + Q_OBJECT +public: + enum Type { + HardDelete = 0, + SoftDelete, + MoveToDeletedItems + }; + + class Response : public EwsRequest::Response + { + public: + protected: + Response(QXmlStreamReader &reader); + + friend class EwsDeleteFolderRequest; + }; + + EwsDeleteFolderRequest(EwsClient &client, QObject *parent); + virtual ~EwsDeleteFolderRequest(); + + void setFolderIds(const EwsId::List &ids) + { + mIds = ids; + }; + void setType(Type type) + { + mType = type; + }; + + virtual void start() override; + + const QList &responses() const + { + return mResponses; + }; +protected: + virtual bool parseResult(QXmlStreamReader &reader) override; + bool parseItemsResponse(QXmlStreamReader &reader); +private: + EwsId::List mIds; + Type mType; + QList mResponses; +}; + +#endif diff --git a/resources/ews/ewsclient/ewsdeletefolderrequest.cpp b/resources/ews/ewsclient/ewsdeletefolderrequest.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsdeletefolderrequest.cpp @@ -0,0 +1,114 @@ +/* + 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 "ewsdeletefolderrequest.h" +#include "ewsclient_debug.h" + +static const QVector deleteTypes = { + QStringLiteral("HardDelete"), + QStringLiteral("SoftDelete"), + QStringLiteral("MoveToDeletedItems") +}; + +EwsDeleteFolderRequest::EwsDeleteFolderRequest(EwsClient &client, QObject *parent) + : EwsRequest(client, parent), mType(SoftDelete) +{ +} + +EwsDeleteFolderRequest::~EwsDeleteFolderRequest() +{ +} + +void EwsDeleteFolderRequest::start() +{ + QString reqString; + QXmlStreamWriter writer(&reqString); + + startSoapDocument(writer); + + writer.writeStartElement(ewsMsgNsUri, QStringLiteral("DeleteFolder")); + + writer.writeAttribute(QStringLiteral("DeleteType"), deleteTypes[mType]); + + writer.writeStartElement(ewsMsgNsUri, QStringLiteral("FolderIds")); + Q_FOREACH (const EwsId &id, mIds) { + id.writeFolderIds(writer); + } + writer.writeEndElement(); + + writer.writeEndElement(); + + endSoapDocument(writer); + + qCDebugNC(EWSCLI_REQUEST_LOG) << QStringLiteral("Starting DeleteFolder request (%1 folders)") + .arg(mIds.size()); + + qCDebug(EWSCLI_PROTO_LOG) << reqString; + + prepare(reqString); + + doSend(); +} + +bool EwsDeleteFolderRequest::parseResult(QXmlStreamReader &reader) +{ + return parseResponseMessage(reader, QStringLiteral("DeleteFolder"), + [this](QXmlStreamReader &reader) {return parseItemsResponse(reader);}); +} + +bool EwsDeleteFolderRequest::parseItemsResponse(QXmlStreamReader &reader) +{ + Response resp(reader); + if (resp.responseClass() == EwsResponseUnknown) { + return false; + } + + if (EWSCLI_REQUEST_LOG().isDebugEnabled()) { + if (resp.isSuccess()) { + qCDebug(EWSCLI_REQUEST_LOG) << QStringLiteral("Got DeleteFolder response - OK"); + } else { + qCDebug(EWSCLI_REQUEST_LOG) << QStringLiteral("Got DeleteFolder response - %1") + .arg(resp.responseMessage()); + } + } + + mResponses.append(resp); + return true; +} + +EwsDeleteFolderRequest::Response::Response(QXmlStreamReader &reader) + : EwsRequest::Response::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())); + return; + } + + if (!readResponseElement(reader)) { + setErrorMsg(QStringLiteral("Failed to read EWS request - invalid response element.")); + return; + } + } +} diff --git a/resources/ews/ewsclient/ewsdeleteitemrequest.h b/resources/ews/ewsclient/ewsdeleteitemrequest.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsdeleteitemrequest.h @@ -0,0 +1,77 @@ +/* + 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 EWSDELETEITEMREQUEST_H +#define EWSDELETEITEMREQUEST_H + +#include + +#include "ewsitem.h" +#include "ewsrequest.h" +#include "ewstypes.h" + +class QXmlStreamReader; + +class EwsDeleteItemRequest : public EwsRequest +{ + Q_OBJECT +public: + enum Type { + HardDelete = 0, + SoftDelete, + MoveToDeletedItems + }; + + class Response : public EwsRequest::Response + { + public: + protected: + Response(QXmlStreamReader &reader); + + friend class EwsDeleteItemRequest; + }; + + EwsDeleteItemRequest(EwsClient &client, QObject *parent); + virtual ~EwsDeleteItemRequest(); + + void setItemIds(const EwsId::List &ids) + { + mIds = ids; + }; + void setType(Type type) + { + mType = type; + }; + + virtual void start() override; + + const QList &responses() const + { + return mResponses; + }; +protected: + virtual bool parseResult(QXmlStreamReader &reader) override; + bool parseItemsResponse(QXmlStreamReader &reader); +private: + EwsId::List mIds; + Type mType; + QList mResponses; +}; + +#endif diff --git a/resources/ews/ewsclient/ewsdeleteitemrequest.cpp b/resources/ews/ewsclient/ewsdeleteitemrequest.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsdeleteitemrequest.cpp @@ -0,0 +1,113 @@ +/* + 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 "ewsdeleteitemrequest.h" +#include "ewsclient_debug.h" + +static const QVector deleteTypes = { + QStringLiteral("HardDelete"), + QStringLiteral("SoftDelete"), + QStringLiteral("MoveToDeletedItems") +}; + +EwsDeleteItemRequest::EwsDeleteItemRequest(EwsClient &client, QObject *parent) + : EwsRequest(client, parent), mType(SoftDelete) +{ +} + +EwsDeleteItemRequest::~EwsDeleteItemRequest() +{ +} + +void EwsDeleteItemRequest::start() +{ + QString reqString; + QXmlStreamWriter writer(&reqString); + + startSoapDocument(writer); + + writer.writeStartElement(ewsMsgNsUri, QStringLiteral("DeleteItem")); + + writer.writeAttribute(QStringLiteral("DeleteType"), deleteTypes[mType]); + + writer.writeStartElement(ewsMsgNsUri, QStringLiteral("ItemIds")); + Q_FOREACH (const EwsId &id, mIds) { + id.writeItemIds(writer); + } + writer.writeEndElement(); + + writer.writeEndElement(); + + endSoapDocument(writer); + + qCDebugNCS(EWSCLI_REQUEST_LOG) << QStringLiteral("Starting DeleteItem request (") << mIds << ")"; + + qCDebug(EWSCLI_PROTO_LOG) << reqString; + + prepare(reqString); + + doSend(); +} + +bool EwsDeleteItemRequest::parseResult(QXmlStreamReader &reader) +{ + return parseResponseMessage(reader, QStringLiteral("DeleteItem"), + [this](QXmlStreamReader &reader) {return parseItemsResponse(reader);}); +} + +bool EwsDeleteItemRequest::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 DeleteItem response - OK, deleted items") << mIds; + } else { + qCDebugNC(EWSCLI_REQUEST_LOG) << QStringLiteral("Got DeleteItem response - %1") + .arg(resp.responseMessage()); + } + } + + mResponses.append(resp); + return true; +} + +EwsDeleteItemRequest::Response::Response(QXmlStreamReader &reader) + : EwsRequest::Response::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())); + return; + } + + if (!readResponseElement(reader)) { + setErrorMsg(QStringLiteral("Failed to read EWS request - invalid response element.")); + return; + } + } +} diff --git a/resources/ews/ewsclient/ewseffectiverights.h b/resources/ews/ewsclient/ewseffectiverights.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewseffectiverights.h @@ -0,0 +1,59 @@ +/* + 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 EWSEFFECTIVERIGHTS_H +#define EWSEFFECTIVERIGHTS_H + +#include +#include + +#include "ewstypes.h" + +class EwsEffectiveRightsPrivate; +class QXmlStreamReader; + +class EwsEffectiveRights +{ +public: + typedef QList List; + + EwsEffectiveRights(); + explicit EwsEffectiveRights(QXmlStreamReader &reader); + EwsEffectiveRights(const EwsEffectiveRights &other); + EwsEffectiveRights(EwsEffectiveRights &&other); + virtual ~EwsEffectiveRights(); + + EwsEffectiveRights &operator=(const EwsEffectiveRights &other); + EwsEffectiveRights &operator=(EwsEffectiveRights &&other); + + bool isValid() const; + bool canCreateAssociated() const; + bool canCreateContents() const; + bool canCreateHierarchy() const; + bool canDelete() const; + bool canModify() const; + bool canRead() const; + bool canViewPrivateItems() const; +protected: + QSharedDataPointer d; +}; + +Q_DECLARE_METATYPE(EwsEffectiveRights) + +#endif diff --git a/resources/ews/ewsclient/ewseffectiverights.cpp b/resources/ews/ewsclient/ewseffectiverights.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewseffectiverights.cpp @@ -0,0 +1,201 @@ +/* + 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 "ewseffectiverights.h" + +#include +#include +#include + +#include "ewsclient_debug.h" +#include "ewstypes.h" + +class EwsEffectiveRightsPrivate : public QSharedData +{ +public: + enum Right { + CreateAssociated = 0, + CreateContents, + CreateHierarchy, + Delete, + Modify, + Read, + ViewPrivateItems + }; + + EwsEffectiveRightsPrivate(); + virtual ~EwsEffectiveRightsPrivate(); + + bool readRight(QXmlStreamReader &reader, Right right); + + bool mValid; + + QBitArray mRights; +}; + +EwsEffectiveRightsPrivate::EwsEffectiveRightsPrivate() + : mValid(false), mRights(7) +{ +} + +EwsEffectiveRightsPrivate::~EwsEffectiveRightsPrivate() +{ +} + +bool EwsEffectiveRightsPrivate::readRight(QXmlStreamReader &reader, Right right) +{ + QString elm = reader.name().toString(); + QString text = reader.readElementText(); + if (reader.error() != QXmlStreamReader::NoError) { + qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid %2 element.") + .arg(QStringLiteral("EffectiveRights")).arg(elm); + return false; + } + + if (text == QStringLiteral("true")) { + mRights.setBit(right); + } else if (text == QStringLiteral("false")) { + mRights.clearBit(right); + } else { + qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid %2 element value: %3.") + .arg(QStringLiteral("EffectiveRights")).arg(elm).arg(text); + return false; + } + + return true; +} + +EwsEffectiveRights::EwsEffectiveRights() + : d(new EwsEffectiveRightsPrivate()) +{ +} + +EwsEffectiveRights::EwsEffectiveRights(QXmlStreamReader &reader) + : d(new EwsEffectiveRightsPrivate()) +{ + while (reader.readNextStartElement()) { + if (reader.namespaceUri() != ewsTypeNsUri) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected namespace in mailbox element:") + << reader.namespaceUri(); + return; + } + + if (reader.name() == QStringLiteral("CreateAssociated")) { + if (!d->readRight(reader, EwsEffectiveRightsPrivate::CreateAssociated)) { + return; + } + } else if (reader.name() == QStringLiteral("CreateContents")) { + if (!d->readRight(reader, EwsEffectiveRightsPrivate::CreateContents)) { + return; + } + } else if (reader.name() == QStringLiteral("CreateHierarchy")) { + if (!d->readRight(reader, EwsEffectiveRightsPrivate::CreateHierarchy)) { + return; + } + } else if (reader.name() == QStringLiteral("Delete")) { + if (!d->readRight(reader, EwsEffectiveRightsPrivate::Delete)) { + return; + } + } else if (reader.name() == QStringLiteral("Modify")) { + if (!d->readRight(reader, EwsEffectiveRightsPrivate::Modify)) { + return; + } + } else if (reader.name() == QStringLiteral("Read")) { + if (!d->readRight(reader, EwsEffectiveRightsPrivate::Read)) { + return; + } + } else if (reader.name() == QStringLiteral("ViewPrivateItems")) { + if (!d->readRight(reader, EwsEffectiveRightsPrivate::ViewPrivateItems)) { + return; + } + } else { + qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - unknown element: %2.") + .arg(QStringLiteral("EffectiveRights")).arg(reader.name().toString()); + return; + } + } + + d->mValid = true; +} + +EwsEffectiveRights::EwsEffectiveRights(const EwsEffectiveRights &other) + : d(other.d) +{ +} + +EwsEffectiveRights::EwsEffectiveRights(EwsEffectiveRights &&other) + : d(std::move(other.d)) +{ +} + +EwsEffectiveRights::~EwsEffectiveRights() +{ +} + +EwsEffectiveRights &EwsEffectiveRights::operator=(const EwsEffectiveRights &other) +{ + d = other.d; + return *this; +} +EwsEffectiveRights &EwsEffectiveRights::operator=(EwsEffectiveRights &&other) +{ + d = std::move(other.d); + return *this; +} + +bool EwsEffectiveRights::isValid() const +{ + return d->mValid; +} + +bool EwsEffectiveRights::canCreateAssociated() const +{ + return d->mRights.testBit(EwsEffectiveRightsPrivate::CreateAssociated); +} + +bool EwsEffectiveRights::canCreateContents() const +{ + return d->mRights.testBit(EwsEffectiveRightsPrivate::CreateContents); +} + +bool EwsEffectiveRights::canCreateHierarchy() const +{ + return d->mRights.testBit(EwsEffectiveRightsPrivate::CreateHierarchy); +} + +bool EwsEffectiveRights::canDelete() const +{ + return d->mRights.testBit(EwsEffectiveRightsPrivate::Delete); +} + +bool EwsEffectiveRights::canModify() const +{ + return d->mRights.testBit(EwsEffectiveRightsPrivate::Modify); +} + +bool EwsEffectiveRights::canRead() const +{ + return d->mRights.testBit(EwsEffectiveRightsPrivate::Read); +} + +bool EwsEffectiveRights::canViewPrivateItems() const +{ + return d->mRights.testBit(EwsEffectiveRightsPrivate::ViewPrivateItems); +} + diff --git a/resources/ews/ewsclient/ewseventrequestbase.h b/resources/ews/ewsclient/ewseventrequestbase.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewseventrequestbase.h @@ -0,0 +1,180 @@ +/* + 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 EWSEVENTREQUESTBASE_H +#define EWSEVENTREQUESTBASE_H + +#include +#include +#include + +#include "ewsid.h" +#include "ewsrequest.h" +#include "ewstypes.h" + +class QXmlStreamReader; +class QXmlStreamWriter; + +class EwsEventRequestBase : public EwsRequest +{ + Q_OBJECT +public: + class Notification; + class Response; + + class Event + { + public: + typedef QList List; + + EwsEventType type() const + { + return mType; + }; + const QString &watermark() const + { + return mWatermark; + }; + const QDateTime ×tamp() const + { + return mTimestamp; + }; + const EwsId &itemId() const + { + return mId; + }; + const EwsId &parentFolderId() const + { + return mParentFolderId; + }; + uint unreadCount() const + { + return mUnreadCount; + }; + const EwsId &oldItemId() const + { + return mOldId; + }; + const EwsId &oldParentFolderId() const + { + return mOldParentFolderId; + }; + bool itemIsFolder() const + { + return mIsFolder; + }; + bool operator==(const Event &other) const; + protected: + Event(QXmlStreamReader &reader); + bool isValid() const + { + return mType != EwsUnknownEvent; + }; + + EwsEventType mType; + QString mWatermark; + QDateTime mTimestamp; + EwsId mId; + EwsId mParentFolderId; + uint mUnreadCount; + EwsId mOldId; + EwsId mOldParentFolderId; + bool mIsFolder; + + friend class EwsEventRequestBase::Notification; + }; + + class Notification + { + public: + typedef QList List; + + const QString &subscriptionId() const + { + return mSubscriptionId; + }; + const QString &previousWatermark() const + { + return mWatermark; + }; + bool hasMoreEvents() const + { + return mMoreEvents; + }; + const Event::List &events() const + { + return mEvents; + }; + bool operator==(const Notification &other) const; + protected: + Notification(QXmlStreamReader &reader); + bool isValid() const + { + return !mSubscriptionId.isNull(); + }; + static bool eventsReader(QXmlStreamReader &reader, QVariant &val); + + QString mSubscriptionId; + QString mWatermark; + bool mMoreEvents; + Event::List mEvents; + + friend class EwsEventRequestBase::Response; + }; + + class Response : public EwsRequest::Response + { + public: + const Notification::List ¬ifications() const + { + return mNotifications; + }; + bool operator==(const Response &other) const; + protected: + Response(QXmlStreamReader &reader); + + Notification::List mNotifications; + + friend class EwsEventRequestBase; + }; + + virtual ~EwsEventRequestBase(); + + void setSubscriptionId(const QString &id) + { + mSubscriptionId = id; + }; + + const QList &responses() const + { + return mResponses; + }; +protected: + EwsEventRequestBase(EwsClient &client, const QString &reqName, QObject *parent); + virtual bool parseResult(QXmlStreamReader &reader) override; + bool parseNotificationsResponse(QXmlStreamReader &reader); + + QString mSubscriptionId; + QList mResponses; + const QString mReqName; +}; + +Q_DECLARE_METATYPE(EwsEventRequestBase::Event::List) + +#endif diff --git a/resources/ews/ewsclient/ewseventrequestbase.cpp b/resources/ews/ewsclient/ewseventrequestbase.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewseventrequestbase.cpp @@ -0,0 +1,269 @@ +/* + 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 "ewseventrequestbase.h" +#include "ewsxml.h" +#include "ewsclient_debug.h" + +enum NotificationElementType { + SubscriptionId, + PreviousWatermark, + MoreEvents, + Events +}; +typedef EwsXml NotificationReader; + +enum EventElementType { + Watermark, + Timestamp, + ItemId, + FolderId, + ParentFolderId, + OldItemId, + OldFolderId, + OldParentFolderId, + UnreadCount +}; +typedef EwsXml EventReader; + +EwsEventRequestBase::EwsEventRequestBase(EwsClient &client, const QString &reqName, QObject *parent) + : EwsRequest(client, parent), mReqName(reqName) +{ + qRegisterMetaType(); +} + +EwsEventRequestBase::~EwsEventRequestBase() +{ +} + +bool EwsEventRequestBase::parseResult(QXmlStreamReader &reader) +{ + return parseResponseMessage(reader, mReqName, + [this](QXmlStreamReader &reader) {return parseNotificationsResponse(reader);}); +} + +bool EwsEventRequestBase::parseNotificationsResponse(QXmlStreamReader &reader) +{ + Response resp(reader); + if (resp.responseClass() == EwsResponseUnknown) { + return false; + } + + if (EWSCLI_REQUEST_LOG().isDebugEnabled()) { + if (resp.isSuccess()) { + uint numEv = 0; + Q_FOREACH (const Notification &nfy, resp.notifications()) { + numEv += nfy.events().size(); + } + qCDebugNC(EWSCLI_REQUEST_LOG) << QStringLiteral("Got %1 response (%2 notifications, %3 events)") + .arg(mReqName).arg(resp.notifications().size()).arg(numEv); + } else { + qCDebug(EWSCLI_REQUEST_LOG) << QStringLiteral("Got %1 response - %2") + .arg(mReqName).arg(resp.responseMessage()); + } + } + + mResponses.append(resp); + return true; +} + +EwsEventRequestBase::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())); + return; + } + + if (reader.name() == QStringLiteral("Notification")) { + Notification nfy(reader); + if (!nfy.isValid()) { + setErrorMsg(QStringLiteral("Failed to process notification.")); + reader.skipCurrentElement(); + return; + } + mNotifications.append(nfy); + } else if (reader.name() == QStringLiteral("Notifications")) { + while (reader.readNextStartElement()) { + if (reader.name() == QStringLiteral("Notification")) { + Notification nfy(reader); + if (!nfy.isValid()) { + setErrorMsg(QStringLiteral("Failed to process notification.")); + reader.skipCurrentElement(); + return; + } + mNotifications.append(nfy); + } else { + setErrorMsg(QStringLiteral("Failed to read EWS request - expected Notification inside Notifications")); + } + } + } else if (reader.name() == QStringLiteral("ConnectionStatus")) { + reader.skipCurrentElement(); + } else if (!readResponseElement(reader)) { + setErrorMsg(QStringLiteral("Failed to read EWS request - invalid response element '%1'") + .arg(reader.name().toString())); + return; + } + } +} + +EwsEventRequestBase::Notification::Notification(QXmlStreamReader &reader) +{ + static const QVector items = { + {SubscriptionId, QStringLiteral("SubscriptionId"), &ewsXmlTextReader}, + {PreviousWatermark, QStringLiteral("PreviousWatermark"), &ewsXmlTextReader}, + {MoreEvents, QStringLiteral("MoreEvents"), &ewsXmlBoolReader}, + {Events, QStringLiteral("CopiedEvent"), &eventsReader}, + {Events, QStringLiteral("CreatedEvent"), &eventsReader}, + {Events, QStringLiteral("DeletedEvent"), &eventsReader}, + {Events, QStringLiteral("ModifiedEvent"), &eventsReader}, + {Events, QStringLiteral("MovedEvent"), &eventsReader}, + {Events, QStringLiteral("NewMailEvent"), &eventsReader}, + {Events, QStringLiteral("FreeBusyChangeEvent"), &eventsReader}, + {Events, QStringLiteral("StatusEvent"), &eventsReader} + }; + static const NotificationReader staticReader(items); + + NotificationReader ewsreader(staticReader); + + if (!ewsreader.readItems(reader, ewsTypeNsUri)) { + return; + } + + QHash values = ewsreader.values(); + + mSubscriptionId = values[SubscriptionId].toString(); + mWatermark = values[PreviousWatermark].toString(); + mMoreEvents = values[MoreEvents].toBool(); + mEvents = values[Events].value(); +} + +bool EwsEventRequestBase::Notification::eventsReader(QXmlStreamReader &reader, QVariant &val) +{ + Event::List events = val.value(); + QString elmName(reader.name().toString()); + + Event event(reader); + if (!event.isValid()) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element").arg(elmName); + return false; + } + + events.append(event); + + val = QVariant::fromValue(events); + return true; +} + +EwsEventRequestBase::Event::Event(QXmlStreamReader &reader) + : mType(EwsUnknownEvent) +{ + static const QVector items = { + {Watermark, QStringLiteral("Watermark"), &ewsXmlTextReader}, + {Timestamp, QStringLiteral("TimeStamp"), &ewsXmlDateTimeReader}, + {FolderId, QStringLiteral("FolderId"), &ewsXmlIdReader}, + {ItemId, QStringLiteral("ItemId"), &ewsXmlIdReader}, + {ParentFolderId, QStringLiteral("ParentFolderId"), &ewsXmlIdReader}, + {OldFolderId, QStringLiteral("OldFolderId"), &ewsXmlIdReader}, + {OldItemId, QStringLiteral("OldItemId"), &ewsXmlIdReader}, + {OldParentFolderId, QStringLiteral("OldParentFolderId"), &ewsXmlIdReader}, + {UnreadCount, QStringLiteral("UnreadCount"), &ewsXmlUIntReader}, + }; + static const EventReader staticReader(items); + + EventReader ewsreader(staticReader); + + QStringRef elmName = reader.name(); + if (elmName == QStringLiteral("CopiedEvent")) { + mType = EwsCopiedEvent; + } else if (elmName == QStringLiteral("CreatedEvent")) { + mType = EwsCreatedEvent; + } else if (elmName == QStringLiteral("DeletedEvent")) { + mType = EwsDeletedEvent; + } else if (elmName == QStringLiteral("ModifiedEvent")) { + mType = EwsModifiedEvent; + } else if (elmName == QStringLiteral("MovedEvent")) { + mType = EwsMovedEvent; + } else if (elmName == QStringLiteral("NewMailEvent")) { + mType = EwsNewMailEvent; + } else if (elmName == QStringLiteral("StatusEvent")) { + mType = EwsStatusEvent; + } else if (elmName == QStringLiteral("FreeBusyChangedEvent")) { + mType = EwsFreeBusyChangedEvent; + } else { + qCWarning(EWSCLI_LOG) << QStringLiteral("Unknown notification event type: %1") + .arg(elmName.toString()); + return; + } + + if (!ewsreader.readItems(reader, ewsTypeNsUri)) { + mType = EwsUnknownEvent; + return; + } + + QHash values = ewsreader.values(); + + mWatermark = values[Watermark].toString(); + mTimestamp = values[Timestamp].value(); + if (values.contains(ItemId)) { + mId = values[ItemId].value(); + mOldId = values[OldItemId].value(); + mIsFolder = false; + } else { + mId = values[FolderId].value(); + mOldId = values[OldFolderId].value(); + mIsFolder = true; + } + mParentFolderId = values[ParentFolderId].value(); + mOldParentFolderId = values[OldParentFolderId].value(); + mUnreadCount = values[UnreadCount].toUInt(); + + if (mType == EwsStatusEvent) { + qCDebugNCS(EWSCLI_LOG) << QStringLiteral(" %1").arg(elmName.toString()); + } else { + qCDebugNCS(EWSCLI_LOG) << QStringLiteral(" %1, %2, parent: ").arg(elmName.toString()).arg(mIsFolder ? 'F' : 'I') + << mParentFolderId << QStringLiteral(", id: ") << mId; + } +} + +bool EwsEventRequestBase::Response::operator==(const Response &other) const +{ + return mNotifications == other.mNotifications; +} + +bool EwsEventRequestBase::Notification::operator==(const Notification &other) const +{ + return (mSubscriptionId == other.mSubscriptionId) && (mWatermark == other.mWatermark) && + (mMoreEvents == other.mMoreEvents) && (mEvents == other.mEvents); +} + +bool EwsEventRequestBase::Event::operator==(const Event &other) const +{ + return (mType == other.mType) && (mWatermark == other.mWatermark) && + (mTimestamp == other.mTimestamp) && (mId == other.mId) && + (mParentFolderId == other.mParentFolderId) && (mUnreadCount == other.mUnreadCount) && + (mOldId == other.mOldId) && (mOldParentFolderId == other.mOldParentFolderId) && + (mIsFolder == other.mIsFolder); +} diff --git a/resources/ews/ewsclient/ewsfindfolderrequest.h b/resources/ews/ewsclient/ewsfindfolderrequest.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsfindfolderrequest.h @@ -0,0 +1,58 @@ +/* + 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. +*/ + +#ifndef EWSFINDFOLDERREQUEST_H +#define EWSFINDFOLDERREQUEST_H + +#include "ewsfolder.h" +#include "ewsrequest.h" +#include "ewstypes.h" +#include "ewsfoldershape.h" + +class EwsFindFolderRequest : public EwsRequest +{ + Q_OBJECT +public: + EwsFindFolderRequest(EwsClient &client, QObject *parent); + virtual ~EwsFindFolderRequest(); + + void setParentFolderId(const EwsId &id); + void setFolderShape(const EwsFolderShape &shape); + void setTraversal(EwsTraversalType traversal) + { + mTraversal = traversal; + }; + + virtual void start() override; + + const QList folders() const + { + return mFolders; + }; +protected: + virtual bool parseResult(QXmlStreamReader &reader) override; + bool parseFoldersResponse(QXmlStreamReader &reader); +private: + EwsId mParentId; + EwsFolderShape mShape; + EwsTraversalType mTraversal; + QList mFolders; +}; + +#endif diff --git a/resources/ews/ewsclient/ewsfindfolderrequest.cpp b/resources/ews/ewsclient/ewsfindfolderrequest.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsfindfolderrequest.cpp @@ -0,0 +1,241 @@ +/* + 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 "ewsfindfolderrequest.h" + +#include + +#include + +#include "ewsclient_debug.h" + +static const QString traversalTypeNames[] = { + QStringLiteral("Shallow"), + QStringLiteral("Deep"), + QStringLiteral("SoftDeleted") +}; + +class EwsFindFolderResponse : public EwsRequest::Response +{ +public: + EwsFindFolderResponse(QXmlStreamReader &reader); + bool parseRootFolder(QXmlStreamReader &reader); + EwsFolder* readFolder(QXmlStreamReader &reader); + unsigned readChildFolders(EwsFolder &parent, unsigned count, QXmlStreamReader &reader); + + QList mFolders; +}; + +EwsFindFolderRequest::EwsFindFolderRequest(EwsClient &client, QObject *parent) + : EwsRequest(client, parent), mTraversal(EwsTraversalDeep) +{ +} + +EwsFindFolderRequest::~EwsFindFolderRequest() +{ +} + +void EwsFindFolderRequest::setParentFolderId(const EwsId &id) +{ + mParentId = id; +} + +void EwsFindFolderRequest::setFolderShape(const EwsFolderShape &shape) +{ + mShape = shape; +} + +void EwsFindFolderRequest::start() +{ + QString reqString; + QXmlStreamWriter writer(&reqString); + + startSoapDocument(writer); + + writer.writeStartElement(ewsMsgNsUri, QStringLiteral("FindFolder")); + writer.writeAttribute(QStringLiteral("Traversal"), traversalTypeNames[mTraversal]); + + mShape.write(writer); + + writer.writeStartElement(ewsMsgNsUri, QStringLiteral("ParentFolderIds")); + mParentId.writeFolderIds(writer); + writer.writeEndElement(); + + writer.writeEndElement(); + + endSoapDocument(writer); + + qCDebug(EWSCLI_PROTO_LOG) << reqString; + + prepare(reqString); + + doSend(); +} + +bool EwsFindFolderRequest::parseResult(QXmlStreamReader &reader) +{ + return parseResponseMessage(reader, QStringLiteral("FindFolder"), + [this](QXmlStreamReader &reader) {return parseFoldersResponse(reader);}); +} + +bool EwsFindFolderRequest::parseFoldersResponse(QXmlStreamReader &reader) +{ + EwsFindFolderResponse *resp = new EwsFindFolderResponse(reader); + if (resp->responseClass() == EwsResponseUnknown) { + return false; + } + + mFolders = resp->mFolders; + + return true; +} + +EwsFindFolderResponse::EwsFindFolderResponse(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())); + return; + } + + if (reader.name() == QStringLiteral("RootFolder")) { + if (!parseRootFolder(reader)) { + return; + } + } else if (!readResponseElement(reader)) { + setErrorMsg(QStringLiteral("Failed to read EWS request - invalid response element.")); + return; + } + } +} + +bool EwsFindFolderResponse::parseRootFolder(QXmlStreamReader &reader) +{ + if (reader.namespaceUri() != ewsMsgNsUri || reader.name() != QStringLiteral("RootFolder")) { + return setErrorMsg(QStringLiteral("Failed to read EWS request - expected %1 element (got %2).") + .arg(QStringLiteral("RootFolder")).arg(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; + unsigned totalItems = reader.attributes().value(QStringLiteral("TotalItemsInView")).toUInt(&ok); + if (!ok) { + return setErrorMsg(QStringLiteral("Failed to read EWS request - failed to read %1 attribute.") + .arg(QStringLiteral("TotalItemsInView"))); + } + + 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() != QStringLiteral("Folders")) { + return setErrorMsg(QStringLiteral("Failed to read EWS request - expected %1 element (got %2).") + .arg(QStringLiteral("Folders")).arg(reader.qualifiedName().toString())); + } + + if (!reader.readNextStartElement()) { + return setErrorMsg(QStringLiteral("Failed to read EWS request - expected a child element in %1 element.") + .arg(QStringLiteral("Folders"))); + } + + if (reader.namespaceUri() != ewsTypeNsUri) { + return setErrorMsg(QStringLiteral("Failed to read EWS request - expected child element from types namespace.")); + } + + unsigned i = 0; + for (i = 0; i < totalItems; ++i) { + EwsFolder *folder = readFolder(reader); + reader.readNextStartElement(); + if (folder) { + bool ok; + int childCount = (*folder)[EwsFolderFieldChildFolderCount].toUInt(&ok); + if (childCount > 0) { + unsigned readCount = readChildFolders(*folder, childCount, reader); + if (readCount == 0) + return false; + i += readCount; + } + mFolders.append(*folder); + } + } + + // Finish the Folders element + reader.skipCurrentElement(); + + // Finish the RootFolder element + reader.skipCurrentElement(); + + return true; +} + +EwsFolder* EwsFindFolderResponse::readFolder(QXmlStreamReader &reader) +{ + EwsFolder *folder = nullptr; + if (reader.name() == QStringLiteral("Folder") || + reader.name() == QStringLiteral("CalendarFolder") || + reader.name() == QStringLiteral("ContactsFolder") || + reader.name() == QStringLiteral("TasksFolder") || + reader.name() == QStringLiteral("SearchFolder")) { + folder = new EwsFolder(reader); + if (!folder->isValid()) { + setErrorMsg(QStringLiteral("Failed to read EWS request - invalid %1 element.") + .arg(QStringLiteral("Folder"))); + return 0; + } + QVariant dn = (*folder)[EwsFolderFieldDisplayName]; + if (!dn.isNull()) { + EwsClient::folderHash[(*folder)[EwsFolderFieldFolderId].value().id()] = dn.toString(); + } + } else { + qCWarning(EWSCLI_LOG).noquote() << QStringLiteral("Unsupported folder type %1").arg(reader.name().toString()); + reader.skipCurrentElement(); + } + + return folder; +} + +unsigned EwsFindFolderResponse::readChildFolders(EwsFolder &parent, unsigned count, QXmlStreamReader &reader) +{ + unsigned readCount = 0; + for (unsigned i = 0; i < count; ++i) { + EwsFolder *folder = readFolder(reader); + reader.readNextStartElement(); + if (folder) { + bool ok; + int childCount = (*folder)[EwsFolderFieldChildFolderCount].toUInt(&ok); + if (ok && childCount > 0) { + unsigned readCount2 = readChildFolders(*folder, childCount, reader); + if (readCount2 == 0) + return false; + readCount += readCount2; + } + parent.addChild(*folder); + } + ++readCount; + } + return readCount; +} + diff --git a/resources/ews/ewsclient/ewsfinditemrequest.h b/resources/ews/ewsclient/ewsfinditemrequest.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsfinditemrequest.h @@ -0,0 +1,103 @@ +/* + 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. +*/ + +#ifndef EWSFINDITEMREQUEST_H +#define EWSFINDITEMREQUEST_H + +#include "ewsitem.h" +#include "ewsrequest.h" +#include "ewstypes.h" +#include "ewsitemshape.h" + +class EwsFindItemRequest : public EwsRequest +{ + Q_OBJECT +public: + EwsFindItemRequest(EwsClient &client, QObject *parent); + virtual ~EwsFindItemRequest(); + + void setFolderId(const EwsId &id); + void setItemShape(const EwsItemShape &shape); + void setTraversal(EwsTraversalType traversal) + { + mTraversal = traversal; + }; + void setPagination(EwsIndexedViewBasePoint basePoint, unsigned offset, int maxItems = -1) + { + mFractional = false; + mMaxItems = maxItems; + mPageBasePoint = basePoint; + mPageOffset = offset; + mPagination = true; + } + void setFractional(unsigned numerator, unsigned denominator, int maxItems = -1) + { + mPagination = false; + mMaxItems = maxItems; + mFracNumerator = numerator; + mFracDenominator = denominator; + mFractional = true; + } + + virtual void start() override; + + bool includesLastItem() const + { + return mIncludesLastItem; + }; + int nextOffset() const + { + return mNextOffset; + }; + int nextNumerator() const + { + return mNextNumerator; + }; + int nextDenominator() const + { + return mNextDenominator; + }; + + const QList items() const + { + return mItems; + }; +protected: + virtual bool parseResult(QXmlStreamReader &reader) override; + bool parseItemsResponse(QXmlStreamReader &reader); +private: + EwsId mFolderId; + EwsItemShape mShape; + EwsTraversalType mTraversal; + bool mPagination; + EwsIndexedViewBasePoint mPageBasePoint; + unsigned mPageOffset; + bool mFractional; + int mMaxItems; + unsigned mFracNumerator; + unsigned mFracDenominator; + QList mItems; + unsigned mTotalItems; + int mNextOffset; + int mNextNumerator; + int mNextDenominator; + bool mIncludesLastItem; +}; + +#endif diff --git a/resources/ews/ewsclient/ewsfinditemrequest.cpp b/resources/ews/ewsclient/ewsfinditemrequest.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsfinditemrequest.cpp @@ -0,0 +1,279 @@ +/* + 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())); + return; + } + + if (reader.name() == QStringLiteral("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() != QStringLiteral("RootFolder")) + return setErrorMsg(QStringLiteral("Failed to read EWS request - expected %1 element (got %2).") + .arg(QStringLiteral("RootFolder")).arg(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")) == QStringLiteral("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() != QStringLiteral("Items")) + return setErrorMsg(QStringLiteral("Failed to read EWS request - expected %1 element (got %2).") + .arg(QStringLiteral("Items")).arg(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 = 0; + if (reader.name() == QStringLiteral("Item") || + reader.name() == QStringLiteral("Message") || + reader.name() == QStringLiteral("CalendarItem") || + reader.name() == QStringLiteral("Contact") || + reader.name() == QStringLiteral("DistributionList") || + reader.name() == QStringLiteral("MeetingMessage") || + reader.name() == QStringLiteral("MeetingRequest") || + reader.name() == QStringLiteral("MeetingResponse") || + reader.name() == QStringLiteral("MeetingCancellation") || + reader.name() == QStringLiteral("Task")) { + qCDebug(EWSCLI_LOG).noquote() << QStringLiteral("Processing %1").arg(reader.name().toString()); + item = new EwsItem(reader); + if (!item->isValid()) { + setErrorMsg(QStringLiteral("Failed to read EWS request - invalid %1 element.") + .arg(reader.name().toString())); + return 0; + } + } else { + qCWarning(EWSCLI_LOG).noquote() << QStringLiteral("Unsupported folder type %1").arg(reader.name().toString()); + reader.skipCurrentElement(); + } + + return item; +} + diff --git a/resources/ews/ewsclient/ewsfolder.h b/resources/ews/ewsclient/ewsfolder.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsfolder.h @@ -0,0 +1,59 @@ +/* + 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. +*/ + +#ifndef EWSFOLDER_H +#define EWSFOLDER_H + +#include "ewsitembase.h" +#include "ewstypes.h" + +class QXmlStreamReader; +class QXmlStreamWriter; +class EwsFolderPrivate; + +class EwsFolder : public EwsItemBase +{ +public: + typedef QList List; + + EwsFolder(); + explicit EwsFolder(QXmlStreamReader &reader); + EwsFolder(const EwsFolder &other); + EwsFolder(EwsFolder &&other); + virtual ~EwsFolder(); + + EwsFolder &operator=(const EwsFolder &other); + EwsFolder &operator=(EwsFolder &&other); + + EwsFolderType type() const; + void setType(EwsFolderType type); + + const QVector childFolders() const; + void addChild(EwsFolder &child); + EwsFolder* parentFolder() const; + void setParentFolder(EwsFolder *parent); + + bool write(QXmlStreamWriter &writer) const; +protected: + bool readBaseFolderElement(QXmlStreamReader &reader); +}; + +Q_DECLARE_METATYPE(EwsFolder) + +#endif diff --git a/resources/ews/ewsclient/ewsfolder.cpp b/resources/ews/ewsclient/ewsfolder.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsfolder.cpp @@ -0,0 +1,233 @@ +/* + 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 "ewsfolder.h" + +#include +#include + +#include "ewsitembase_p.h" +#include "ewsxml.h" +#include "ewseffectiverights.h" +#include "ewsclient_debug.h" + +#define D_PTR EwsFolderPrivate *d = reinterpret_cast(this->d.data()); +#define D_CPTR const EwsFolderPrivate *d = reinterpret_cast(this->d.data()); + +static const QVector folderTypeNames = { + QStringLiteral("Folder"), + QStringLiteral("CalendarFolder"), + QStringLiteral("ContactsFolder"), + QStringLiteral("SearchFolder"), + QStringLiteral("TasksFolder") +}; + +class EwsFolderPrivate : public EwsItemBasePrivate +{ +public: + typedef EwsXml XmlProc; + + EwsFolderPrivate(); + EwsFolderPrivate(const EwsItemBasePrivate &other); + virtual EwsItemBasePrivate *clone() const override + { + return new EwsFolderPrivate(*this); + } + + static bool effectiveRightsReader(QXmlStreamReader &reader, QVariant &val); + + EwsFolderType mType; + EwsFolder *mParent; + QVector mChildren; + static const XmlProc mStaticEwsXml; + XmlProc mEwsXml; +}; + +typedef EwsXml ItemFieldsReader; + +static const QVector ewsFolderItems = { + {EwsFolderFieldFolderId, QStringLiteral("FolderId"), &ewsXmlIdReader, &ewsXmlIdWriter}, + {EwsFolderFieldParentFolderId, QStringLiteral("ParentFolderId"), &ewsXmlIdReader}, + {EwsFolderFieldFolderClass, QStringLiteral("FolderClass"), &ewsXmlTextReader, &ewsXmlTextWriter}, + {EwsFolderFieldDisplayName, QStringLiteral("DisplayName"), &ewsXmlTextReader, &ewsXmlTextWriter}, + {EwsFolderFieldTotalCount, QStringLiteral("TotalCount"), &ewsXmlUIntReader, &ewsXmlUIntWriter}, + {EwsFolderFieldChildFolderCount, QStringLiteral("ChildFolderCount"), &ewsXmlUIntReader}, + {EwsItemFieldExtendedProperties, QStringLiteral("ExtendedProperty"), + &EwsItemBasePrivate::extendedPropertyReader, &EwsItemBasePrivate::extendedPropertyWriter}, + {EwsFolderFieldUnreadCount, QStringLiteral("UnreadCount"), &ewsXmlUIntReader}, + {EwsFolderPrivate::XmlProc::Ignore, QStringLiteral("SearchParameters")}, + {EwsFolderFieldEffectiveRights, QStringLiteral("EffectiveRights"), + &EwsFolderPrivate::effectiveRightsReader}, + {EwsFolderPrivate::XmlProc::Ignore, QStringLiteral("ManagedFolderInformation")}, +}; + +const EwsFolderPrivate::XmlProc EwsFolderPrivate::mStaticEwsXml(ewsFolderItems); + +EwsFolderPrivate::EwsFolderPrivate() + : EwsItemBasePrivate(), mType(EwsFolderTypeUnknown), mParent(0), mEwsXml(mStaticEwsXml) +{ +} + +bool EwsFolderPrivate::effectiveRightsReader(QXmlStreamReader &reader, QVariant &val) +{ + EwsEffectiveRights rights(reader); + if (!rights.isValid()) { + reader.skipCurrentElement(); + return false; + } + val = QVariant::fromValue(rights); + return true; +} + +EwsFolder::EwsFolder() + : EwsItemBase(QSharedDataPointer(new EwsFolderPrivate())) +{ +} + +EwsFolder::EwsFolder(QXmlStreamReader &reader) + : EwsItemBase(QSharedDataPointer(new EwsFolderPrivate())) +{ + D_PTR + + // Check what item type are we + uint i = 0; + d->mType = EwsFolderTypeUnknown; + Q_FOREACH (const QString &name, folderTypeNames) { + if (name == reader.name()) { + d->mType = static_cast(i); + break; + } + i++; + } + if (d->mType == EwsFolderTypeUnknown) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unknown folder type %1").arg(reader.name().toString()); + } + + while (reader.readNextStartElement()) { + if (reader.namespaceUri() != ewsTypeNsUri) { + qCWarningNC(EWSCLI_LOG) << "Unexpected namespace in folder element:" + << reader.namespaceUri(); + return; + } + + if (!readBaseFolderElement(reader)) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Invalid folder child: %1").arg(reader.qualifiedName().toString()); + return; + } + } + d->mValid = true; + +} + +EwsFolder::EwsFolder(const EwsFolder &other) + : EwsItemBase(other.d) +{ + qRegisterMetaType(); +} + +EwsFolder::EwsFolder(EwsFolder &&other) + : EwsItemBase(other.d) +{ +} + +EwsFolder::~EwsFolder() +{ +} + +EwsFolderType EwsFolder::type() const +{ + D_CPTR + return d->mType; +} + +void EwsFolder::setType(EwsFolderType type) +{ + D_PTR + d->mType = type; +} + +bool EwsFolder::readBaseFolderElement(QXmlStreamReader &reader) +{ + D_PTR + + if (!d->mEwsXml.readItem(reader, QStringLiteral("Folder"), ewsTypeNsUri)) { + return false; + } + + d->mFields = d->mEwsXml.values(); + + return true; +} + +const QVector EwsFolder::childFolders() const +{ + D_CPTR + return d->mChildren; +} + +void EwsFolder::addChild(EwsFolder &child) +{ + D_PTR + + if (child.parentFolder() != nullptr) { + qCWarning(EWSCLI_LOG).noquote() + << QStringLiteral("Attempt to add child folder which already has a parent (child: %1)"). + arg(child[EwsFolderFieldFolderId].value().id()); + } + d->mChildren.append(child); + child.setParentFolder(this); +} + +EwsFolder* EwsFolder::parentFolder() const +{ + D_CPTR + return d->mParent; +} + +void EwsFolder::setParentFolder(EwsFolder *parent) +{ + D_PTR + d->mParent = parent; +} + +EwsFolder &EwsFolder::operator=(const EwsFolder &other) +{ + d = other.d; + return *this; +} + +EwsFolder &EwsFolder::operator=(EwsFolder &&other) +{ + d = std::move(other.d); + return *this; +} + +bool EwsFolder::write(QXmlStreamWriter &writer) const +{ + D_CPTR + + writer.writeStartElement(ewsTypeNsUri, folderTypeNames[d->mType]); + + bool status = d->mEwsXml.writeItems(writer, folderTypeNames[d->mType], ewsTypeNsUri, d->mFields); + + writer.writeEndElement(); + + return status; +} + diff --git a/resources/ews/ewsclient/ewsfoldershape.h b/resources/ews/ewsclient/ewsfoldershape.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsfoldershape.h @@ -0,0 +1,69 @@ +/* + 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 EWSFOLDERSHAPE_H +#define EWSFOLDERSHAPE_H + +#include +#include +#include + +#include "ewspropertyfield.h" +#include "ewstypes.h" + +class EwsFolderShape +{ +public: + explicit EwsFolderShape(EwsBaseShape shape = EwsShapeDefault) : mBaseShape(shape) {}; + ~EwsFolderShape() {}; + EwsFolderShape(const EwsFolderShape &other) + : mBaseShape(other.mBaseShape), mProps(other.mProps) {}; + EwsFolderShape(EwsFolderShape &&other) + : mBaseShape(other.mBaseShape), mProps(other.mProps) {}; + EwsFolderShape &operator=(EwsFolderShape &&other) + { + mBaseShape = other.mBaseShape; + mProps = std::move(other.mProps); + return *this; + } + EwsFolderShape &operator=(const EwsFolderShape &other) + { + mBaseShape = other.mBaseShape; + mProps = other.mProps; + return *this; + } + + void write(QXmlStreamWriter &writer) const; + + friend EwsFolderShape &operator<<(EwsFolderShape &shape, const EwsPropertyField &prop); +protected: + void writeBaseShape(QXmlStreamWriter &writer) const; + void writeProperties(QXmlStreamWriter &writer) const; + + EwsBaseShape mBaseShape; + QVector mProps; +}; + +inline EwsFolderShape &operator<<(EwsFolderShape &shape, const EwsPropertyField &prop) +{ + shape.mProps.append(prop); + return shape; +} + +#endif diff --git a/resources/ews/ewsclient/ewsfoldershape.cpp b/resources/ews/ewsclient/ewsfoldershape.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsfoldershape.cpp @@ -0,0 +1,57 @@ +/* + 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 "ewsfoldershape.h" + +static const QString shapeNames[] = { + QStringLiteral("IdOnly"), + QStringLiteral("Default"), + QStringLiteral("AllProperties") +}; + +void EwsFolderShape::write(QXmlStreamWriter &writer) const +{ + writer.writeStartElement(ewsMsgNsUri, QStringLiteral("FolderShape")); + + // Write the base shape + writeBaseShape(writer); + + // Write properties (if any) + writeProperties(writer); + + writer.writeEndElement(); +} + +void EwsFolderShape::writeBaseShape(QXmlStreamWriter &writer) const +{ + writer.writeTextElement(ewsTypeNsUri, QStringLiteral("BaseShape"), shapeNames[mBaseShape]); +} + +void EwsFolderShape::writeProperties(QXmlStreamWriter &writer) const +{ + if (!mProps.isEmpty()) { + writer.writeStartElement(ewsTypeNsUri, QStringLiteral("AdditionalProperties")); + + foreach (const EwsPropertyField &prop, mProps) { + prop.write(writer); + } + + writer.writeEndElement(); + } +} diff --git a/resources/ews/ewsclient/ewsgeteventsrequest.h b/resources/ews/ewsclient/ewsgeteventsrequest.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsgeteventsrequest.h @@ -0,0 +1,52 @@ +/* + 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. +*/ + +#ifndef EWSGETEVENTSREQUEST_H +#define EWSGETEVENTSREQUEST_H + +#include +#include +#include + +#include "ewsid.h" +#include "ewseventrequestbase.h" +#include "ewstypes.h" + +class QXmlStreamReader; +class QXmlStreamWriter; + +class EwsGetEventsRequest : public EwsEventRequestBase +{ + Q_OBJECT +public: + EwsGetEventsRequest(EwsClient &client, QObject *parent); + virtual ~EwsGetEventsRequest(); + + void setWatermark(const QString &watermark) + { + mWatermark = watermark; + }; + + virtual void start() override; + +protected: + QString mWatermark; +}; + +#endif diff --git a/resources/ews/ewsclient/ewsgeteventsrequest.cpp b/resources/ews/ewsclient/ewsgeteventsrequest.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsgeteventsrequest.cpp @@ -0,0 +1,58 @@ +/* + 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 "ewsgeteventsrequest.h" +#include "ewsxml.h" +#include "ewsclient_debug.h" + +EwsGetEventsRequest::EwsGetEventsRequest(EwsClient &client, QObject *parent) + : EwsEventRequestBase(client, QStringLiteral("GetEvents"), parent) +{ +} + +EwsGetEventsRequest::~EwsGetEventsRequest() +{ +} + +void EwsGetEventsRequest::start() +{ + QString reqString; + QXmlStreamWriter writer(&reqString); + + startSoapDocument(writer); + + writer.writeStartElement(ewsMsgNsUri, QStringLiteral("GetEvents")); + + writer.writeTextElement(ewsMsgNsUri, QStringLiteral("SubscriptionId"), mSubscriptionId); + + writer.writeTextElement(ewsMsgNsUri, QStringLiteral("Watermark"), mWatermark); + + writer.writeEndElement(); + + endSoapDocument(writer); + + qCDebugNC(EWSCLI_REQUEST_LOG) << QStringLiteral("Starting GetEvents request (subId: %1, wmark: %2)") + .arg(mSubscriptionId).arg(mWatermark); + + qCDebug(EWSCLI_PROTO_LOG) << reqString; + + prepare(reqString); + + doSend(); +} diff --git a/resources/ews/ewsclient/ewsgetfolderrequest.h b/resources/ews/ewsclient/ewsgetfolderrequest.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsgetfolderrequest.h @@ -0,0 +1,67 @@ +/* + 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. +*/ + +#ifndef EWSGETFOLDERREQUEST_H +#define EWSGETFOLDERREQUEST_H + +#include "ewsfolder.h" +#include "ewsrequest.h" +#include "ewstypes.h" +#include "ewsfoldershape.h" + +class EwsGetFolderRequest : public EwsRequest +{ + Q_OBJECT +public: + class Response : public EwsRequest::Response + { + public: + explicit Response(QXmlStreamReader &reader); + bool parseFolders(QXmlStreamReader &reader); + + const EwsFolder &folder() const + { + return mFolder; + }; + private: + EwsFolder mFolder; + }; + + EwsGetFolderRequest(EwsClient &client, QObject *parent); + virtual ~EwsGetFolderRequest(); + + void setFolderIds(const EwsId::List &ids); + void setFolderShape(const EwsFolderShape &shape); + + virtual void start() override; + + const QList &responses() const + { + return mResponses; + }; +protected: + virtual bool parseResult(QXmlStreamReader &reader) override; + bool parseFoldersResponse(QXmlStreamReader &reader); +private: + EwsId::List mIds; + EwsFolderShape mShape; + QList mResponses; +}; + +#endif diff --git a/resources/ews/ewsclient/ewsgetfolderrequest.cpp b/resources/ews/ewsclient/ewsgetfolderrequest.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsgetfolderrequest.cpp @@ -0,0 +1,155 @@ +/* + 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 "ewsgetfolderrequest.h" + +#include + +#include "ewsclient_debug.h" + +EwsGetFolderRequest::EwsGetFolderRequest(EwsClient &client, QObject *parent) + : EwsRequest(client, parent) +{ +} + +EwsGetFolderRequest::~EwsGetFolderRequest() +{ +} + +void EwsGetFolderRequest::setFolderIds(const EwsId::List &ids) +{ + mIds = ids; +} + +void EwsGetFolderRequest::setFolderShape(const EwsFolderShape &shape) +{ + mShape = shape; +} + +void EwsGetFolderRequest::start() +{ + QString reqString; + QXmlStreamWriter writer(&reqString); + + startSoapDocument(writer); + + writer.writeStartElement(ewsMsgNsUri, QStringLiteral("GetFolder")); + + mShape.write(writer); + + writer.writeStartElement(ewsMsgNsUri, QStringLiteral("FolderIds")); + Q_FOREACH (const EwsId &id, mIds) { + id.writeFolderIds(writer); + } + writer.writeEndElement(); + + writer.writeEndElement(); + + endSoapDocument(writer); + + qCDebug(EWSCLI_PROTO_LOG) << reqString; + + qCDebugNCS(EWSCLI_REQUEST_LOG) << QStringLiteral("Starting GetFolder request (") << mIds << ")"; + + prepare(reqString); + + doSend(); +} + +bool EwsGetFolderRequest::parseResult(QXmlStreamReader &reader) +{ + return parseResponseMessage(reader, QStringLiteral("GetFolder"), + [this](QXmlStreamReader &reader) {return parseFoldersResponse(reader);}); +} + +bool EwsGetFolderRequest::parseFoldersResponse(QXmlStreamReader &reader) +{ + Response resp(reader); + if (resp.responseClass() == EwsResponseUnknown) { + return false; + } + + mResponses.append(resp); + if (EWSCLI_REQUEST_LOG().isDebugEnabled()) { + if (resp.isSuccess()) { + const EwsFolder &folder = resp.folder(); + const EwsId &id = folder[EwsFolderFieldFolderId].value(); + qCDebugNC(EWSCLI_REQUEST_LOG) << QStringLiteral("Got GetFolder response (id: %1, name: %2)") + .arg(ewsHash(id.id())).arg(folder[EwsFolderFieldDisplayName].toString()); + } else { + qCDebugNC(EWSCLI_REQUEST_LOG) << QStringLiteral("Got GetFolder response - %1") + .arg(resp.responseMessage()); + } + } + + QVariant dn = resp.folder()[EwsFolderFieldDisplayName]; + if (!dn.isNull()) { + EwsClient::folderHash[resp.folder()[EwsFolderFieldFolderId].value().id()] = dn.toString(); + } + + return true; +} + +EwsGetFolderRequest::Response::Response(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())); + return; + } + + if (reader.name() == QStringLiteral("Folders")) { + if (!parseFolders(reader)) { + return; + } + } else if (!readResponseElement(reader)) { + setErrorMsg(QStringLiteral("Failed to read EWS request - invalid response element.")); + return; + } + } +} + +bool EwsGetFolderRequest::Response::parseFolders(QXmlStreamReader &reader) +{ + if (reader.namespaceUri() != ewsMsgNsUri || reader.name() != QStringLiteral("Folders")) { + return setErrorMsg(QStringLiteral("Failed to read EWS request - expected Folders element (got %1).") + .arg(reader.qualifiedName().toString())); + } + + if (!reader.readNextStartElement()) { + return setErrorMsg(QStringLiteral("Failed to read EWS request - expected a child element in Folders element.")); + } + + if (reader.namespaceUri() != ewsTypeNsUri) { + return setErrorMsg(QStringLiteral("Failed to read EWS request - expected child element from types namespace.")); + } + + EwsFolder folder(reader); + if (!folder.isValid()) { + return setErrorMsg(QStringLiteral("Failed to read EWS request - invalid Folder element.")); + } + mFolder = folder; + + // Finish the Folders element + reader.skipCurrentElement(); + + return true; +} diff --git a/resources/ews/ewsclient/ewsgetitemrequest.h b/resources/ews/ewsclient/ewsgetitemrequest.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsgetitemrequest.h @@ -0,0 +1,68 @@ +/* + 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 EWSGETITEMREQUEST_H +#define EWSGETITEMREQUEST_H + +#include + +#include "ewsitem.h" +#include "ewsitemshape.h" +#include "ewsrequest.h" +#include "ewstypes.h" + +class EwsGetItemRequest : public EwsRequest +{ + Q_OBJECT +public: + class Response : public EwsRequest::Response + { + public: + explicit Response(QXmlStreamReader &reader); + bool parseItems(QXmlStreamReader &reader); + const EwsItem &item() const + { + return mItem; + }; + private: + EwsItem mItem; + }; + + EwsGetItemRequest(EwsClient &client, QObject *parent); + virtual ~EwsGetItemRequest(); + + void setItemIds(const EwsId::List &ids); + void setItemShape(const EwsItemShape &shape); + + virtual void start() override; + + const QList &responses() const + { + return mResponses; + }; +protected: + virtual bool parseResult(QXmlStreamReader &reader) override; + bool parseItemsResponse(QXmlStreamReader &reader); +private: + EwsId::List mIds; + EwsItemShape mShape; + QList mResponses; +}; + +#endif diff --git a/resources/ews/ewsclient/ewsgetitemrequest.cpp b/resources/ews/ewsclient/ewsgetitemrequest.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsgetitemrequest.cpp @@ -0,0 +1,148 @@ +/* + 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")); + Q_FOREACH (const EwsId &id, 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())).arg(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())); + return; + } + + if (reader.name() == QStringLiteral("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() != QStringLiteral("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 new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsgetstreamingeventsrequest.h @@ -0,0 +1,59 @@ +/* + 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 + +#include "ewseventrequestbase.h" +#include "ewsid.h" +#include "ewstypes.h" + +class QXmlStreamReader; +class QXmlStreamWriter; + +class EwsGetStreamingEventsRequest : public EwsEventRequestBase +{ + Q_OBJECT +public: + EwsGetStreamingEventsRequest(EwsClient &client, QObject *parent); + virtual ~EwsGetStreamingEventsRequest(); + + void setTimeout(uint timeout) + { + mTimeout = timeout; + }; + + virtual void start() override; +public Q_SLOTS: + void eventsProcessed(const Response &response); +Q_SIGNALS: + void eventsReceived(KJob *job); +protected Q_SLOTS: + void requestData(KIO::Job *job, const QByteArray &data); + void requestDataTimeout(); +protected: + uint mTimeout; + QTimer mRespTimer; +}; + +#endif diff --git a/resources/ews/ewsclient/ewsgetstreamingeventsrequest.cpp b/resources/ews/ewsclient/ewsgetstreamingeventsrequest.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsgetstreamingeventsrequest.cpp @@ -0,0 +1,118 @@ +/* + 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 "ewsgetstreamingeventsrequest.h" + +#include + +#include "ewsclient_debug.h" +#include "ewsxml.h" + +static Q_CONSTEXPR uint respChunkTimeout = 250; /* ms */ + +EwsGetStreamingEventsRequest::EwsGetStreamingEventsRequest(EwsClient &client, QObject *parent) + : EwsEventRequestBase(client, QStringLiteral("GetStreamingEvents"), parent), mTimeout(30), + mRespTimer(this) +{ + mRespTimer.setInterval(respChunkTimeout); + connect(&mRespTimer, &QTimer::timeout, this, &EwsGetStreamingEventsRequest::requestDataTimeout); +} + +EwsGetStreamingEventsRequest::~EwsGetStreamingEventsRequest() +{ +} + +void EwsGetStreamingEventsRequest::start() +{ + QString reqString; + QXmlStreamWriter writer(&reqString); + + if (!serverVersion().supports(EwsServerVersion::StreamingSubscription)) { + setServerVersion(EwsServerVersion::minSupporting(EwsServerVersion::StreamingSubscription)); + } + + startSoapDocument(writer); + + writer.writeStartElement(ewsMsgNsUri, QStringLiteral("GetStreamingEvents")); + + writer.writeStartElement(ewsMsgNsUri, QStringLiteral("SubscriptionIds")); + writer.writeTextElement(ewsTypeNsUri, QStringLiteral("SubscriptionId"), mSubscriptionId); + writer.writeEndElement(); + + writer.writeTextElement(ewsMsgNsUri, QStringLiteral("ConnectionTimeout"), QString::number(mTimeout)); + + writer.writeEndElement(); + + endSoapDocument(writer); + + qCDebugNC(EWSCLI_REQUEST_LOG) << QStringLiteral("Starting GetStreamingEvents request (subId: %1, timeout: %2)") + .arg(ewsHash(mSubscriptionId)).arg(mTimeout); + + qCDebug(EWSCLI_PROTO_LOG) << reqString; + + prepare(reqString); + + doSend(); +} + +void EwsGetStreamingEventsRequest::requestData(KIO::Job *job, const QByteArray &data) +{ + Q_UNUSED(job); + + mRespTimer.stop(); + qCDebug(EWSCLI_PROTO_LOG) << "data" << job << data; + mResponseData += QString::fromUtf8(data); + mRespTimer.start(); +} + +void EwsGetStreamingEventsRequest::requestDataTimeout() +{ + if (mResponseData.isEmpty()) { + return; + } + 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(); + } + } + + QXmlStreamReader reader(mResponseData); + if (!readResponse(reader)) { + Q_FOREACH (KJob *job, subjobs()) { + removeSubjob(job); + job->kill(); + } + emitResult(); + } else { + Q_EMIT eventsReceived(this); + } + + mResponseData.clear(); +} + +void EwsGetStreamingEventsRequest::eventsProcessed(const Response &resp) +{ + mResponses.removeOne(resp); +} diff --git a/resources/ews/ewsclient/ewsid.h b/resources/ews/ewsclient/ewsid.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsid.h @@ -0,0 +1,117 @@ +/* + 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 EWSID_H +#define EWSID_H + +#include +#include +#include + +#include "ewstypes.h" + +#include + +class QXmlStreamWriter; +class QXmlStreamReader; +/** + * @brief EWS Id wrapper class + * + * This class wraps an EWS folder or item identifier. + * + * In the EWS world an id can come in two forms: + * - An "actual" id identified by a unique, server-generated string (actually it's a + * base64-encoded internal server structure). Optionally this id is accompanied by a change + * key, which acts as a version number of the item. Each time something changes with the + * item (either the item itself or folder content - not sure if this applies to subfolders) + * the change key is updated. This gives you access to an older version of the item and + * allows to quickly find out if the item needs synchronizing. + * - A "distinguished" folder id which is a string identifying a list of known root folders + * such as 'inbox'. This is necessary for the initial query as there is no way to know the + * real folder ids beforehand. This applies only to folder identifiers. + */ +class EwsId +{ +public: + enum Type { + Distinguished, + Real, + Unspecified + }; + + typedef QList List; + + explicit EwsId(EwsDistinguishedId did) : mType(Distinguished), mDid(did) {}; + explicit EwsId(const QString &id, const QString &changeKey = QString()); + EwsId(const EwsId &id) + { + *this = id; + }; + EwsId(EwsId &&id) + { + *this = std::move(id); + }; + EwsId() : mType(Unspecified), mDid(EwsDIdCalendar) {}; + explicit EwsId(QXmlStreamReader &reader); + + Type type() const + { + return mType; + }; + QString id() const + { + return mId; + }; + QString changeKey() const + { + return mChangeKey; + }; + EwsDistinguishedId distinguishedId() const + { + return mDid; + }; + + EwsId &operator=(const EwsId &other); + EwsId &operator=(EwsId &&other); + bool operator==(const EwsId &other) const; + bool operator<(const EwsId &other) const; + + void writeFolderIds(QXmlStreamWriter &writer) const; + void writeItemIds(QXmlStreamWriter &writer) const; + void writeAttributes(QXmlStreamWriter &writer) const; + + friend QDebug operator<<(QDebug debug, const EwsId &id); +#ifdef HAVE_INBOX_FILTERING_WORKAROUND + static void setInboxId(EwsId id); +#endif +private: + Type mType; + QString mId; + QString mChangeKey; + EwsDistinguishedId mDid; +}; + +uint qHash(const EwsId &id, uint seed); + +QDebug operator<<(QDebug debug, const EwsId &id); + +Q_DECLARE_METATYPE(EwsId) +Q_DECLARE_METATYPE(EwsId::List) + +#endif diff --git a/resources/ews/ewsclient/ewsid.cpp b/resources/ews/ewsclient/ewsid.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsid.cpp @@ -0,0 +1,267 @@ +/* + 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 "ewsid.h" + +#include +#include +#include +#include + +#include "ewsclient.h" +#include "ewsclient_debug.h" + +static const QString distinguishedIdNames[] = { + QStringLiteral("calendar"), + QStringLiteral("contacts"), + QStringLiteral("deleteditems"), + QStringLiteral("drafts"), + QStringLiteral("inbox"), + QStringLiteral("journal"), + QStringLiteral("notes"), + QStringLiteral("outbox"), + QStringLiteral("sentitems"), + QStringLiteral("tasks"), + QStringLiteral("msgfolderroot"), + QStringLiteral("root"), + QStringLiteral("junkemail"), + QStringLiteral("searchfolders"), + QStringLiteral("voicemail"), + QStringLiteral("recoverableitemsroot"), + QStringLiteral("recoverableitemsdeletions"), + QStringLiteral("recoverableitemsversions"), + QStringLiteral("recoverableitemspurges"), + QStringLiteral("archiveroot"), + QStringLiteral("archivemsgfolderroot"), + QStringLiteral("archivedeleteditems"), + QStringLiteral("archiverecoverableitemsroot"), + QStringLiteral("archiverecoverableitemsdeletions"), + QStringLiteral("archiverecoverableitemsversions"), + QStringLiteral("archiverecoverableitemspurges") +}; + +class EwsIdComparatorRegistrar +{ +public: + EwsIdComparatorRegistrar() + { + QMetaType::registerComparators(); + }; +}; +const EwsIdComparatorRegistrar ewsIdComparatorRegistrar; + +/* + * Current Akonadi mail filter agent determines which folders to filter for incoming mail by + * checking if their remote ID is "INBOX". This is very IMAP-centric and obviously will not work + * with EWS, which uses Base64 folder identifiers. + * Until Akonadi is fixed pretend that the remote identifier for the inbox folder is "INBOX" and + * hide the real identifier in a global variable. + */ +#ifdef HAVE_INBOX_FILTERING_WORKAROUND +static QString inboxId; +#endif + +EwsId::EwsId(QXmlStreamReader &reader) + : mDid(EwsDIdCalendar) +{ + // Don't check for this element's name as a folder id may be contained in several elements + // such as "FolderId" or "ParentFolderId". + const QXmlStreamAttributes &attrs = reader.attributes(); + + QStringRef idRef = attrs.value(QStringLiteral("Id")); + QStringRef changeKeyRef = attrs.value(QStringLiteral("ChangeKey")); + if (idRef.isNull()) + return; + + mId = idRef.toString(); + if (!changeKeyRef.isNull()) + mChangeKey = changeKeyRef.toString(); +#ifdef HAVE_INBOX_FILTERING_WORKAROUND + if (mId == inboxId) { + mId = QStringLiteral("INBOX"); + } +#endif + mType = Real; +} + +EwsId::EwsId(const QString &id, const QString &changeKey) + : mType(Real), mId(id), mChangeKey(changeKey), mDid(EwsDIdCalendar) +{ +#ifdef HAVE_INBOX_FILTERING_WORKAROUND + if (mId == inboxId) { + mId = QStringLiteral("INBOX"); + } +#endif +} + +EwsId &EwsId::operator=(const EwsId &other) +{ + mType = other.mType; + if (mType == Distinguished) { + mDid = other.mDid; + } else if (mType == Real) { + mId = other.mId; + mChangeKey = other.mChangeKey; + } + return *this; +} + +EwsId &EwsId::operator=(EwsId &&other) +{ + mType = other.mType; + if (mType == Distinguished) { + mDid = other.mDid; + } else if (mType == Real) { + mId = std::move(other.mId); + mChangeKey = std::move(other.mChangeKey); + } + return *this; +} + +bool EwsId::operator==(const EwsId &other) const +{ + if (mType != other.mType) + return false; + + if (mType == Distinguished) { + return (mDid == other.mDid); + } else if (mType == Real) { + return (mId == other.mId && mChangeKey == other.mChangeKey); + } + return true; +} + +bool EwsId::operator<(const EwsId &other) const +{ + if (mType != other.mType) + return mType < other.mType; + + if (mType == Distinguished) { + return (mDid < other.mDid); + } else if (mType == Real) { + return (mId < other.mId && mChangeKey < other.mChangeKey); + } + return false; +} + +void EwsId::writeFolderIds(QXmlStreamWriter &writer) const +{ + if (mType == Distinguished) { + writer.writeStartElement(ewsTypeNsUri, QStringLiteral("DistinguishedFolderId")); + writer.writeAttribute(QStringLiteral("Id"), distinguishedIdNames[mDid]); + writer.writeEndElement(); + } else if (mType == Real) { +#ifdef HAVE_INBOX_FILTERING_WORKAROUND + if (mId == QStringLiteral("INBOX")) { + if (inboxId.isEmpty()) { + writer.writeStartElement(ewsTypeNsUri, QStringLiteral("DistinguishedFolderId")); + writer.writeAttribute(QStringLiteral("Id"), distinguishedIdNames[EwsDIdInbox]); + } else { + writer.writeStartElement(ewsTypeNsUri, QStringLiteral("FolderId")); + writer.writeAttribute(QStringLiteral("Id"), inboxId); + } + } else { + writer.writeStartElement(ewsTypeNsUri, QStringLiteral("FolderId")); + writer.writeAttribute(QStringLiteral("Id"), mId); + } +#else + writer.writeStartElement(ewsTypeNsUri, QStringLiteral("FolderId")); + writer.writeAttribute(QStringLiteral("Id"), mId); +#endif + if (!mChangeKey.isEmpty()) { + writer.writeAttribute(QStringLiteral("ChangeKey"), mChangeKey); + } + writer.writeEndElement(); + } +} + +void EwsId::writeItemIds(QXmlStreamWriter &writer) const +{ + if (mType == Real) { + writer.writeStartElement(ewsTypeNsUri, QStringLiteral("ItemId")); +#ifdef HAVE_INBOX_FILTERING_WORKAROUND + if (mId == QStringLiteral("INBOX")) { + writer.writeAttribute(QStringLiteral("Id"), inboxId); + } else { + writer.writeAttribute(QStringLiteral("Id"), mId); + } +#else + writer.writeAttribute(QStringLiteral("Id"), mId); +#endif + if (!mChangeKey.isEmpty()) { + writer.writeAttribute(QStringLiteral("ChangeKey"), mChangeKey); + } + writer.writeEndElement(); + } +} + +void EwsId::writeAttributes(QXmlStreamWriter &writer) const +{ + if (mType == Real) { +#ifdef HAVE_INBOX_FILTERING_WORKAROUND + if (mId == QStringLiteral("INBOX")) { + writer.writeAttribute(QStringLiteral("Id"), inboxId); + } else { + writer.writeAttribute(QStringLiteral("Id"), mId); + } +#else + writer.writeAttribute(QStringLiteral("Id"), mId); +#endif + if (!mChangeKey.isEmpty()) { + writer.writeAttribute(QStringLiteral("ChangeKey"), mChangeKey); + } + } +} + +QDebug operator<<(QDebug debug, const EwsId &id) +{ + QDebugStateSaver saver(debug); + QDebug d = debug.nospace().noquote(); + d << QStringLiteral("EwsId("); + + switch (id.mType) { + case EwsId::Distinguished: + d << QStringLiteral("Distinguished: ") << distinguishedIdNames[id.mDid]; + break; + case EwsId::Real: { + QString name = EwsClient::folderHash.value(id.mId, ewsHash(id.mId)); + d << name << QStringLiteral(", ") << ewsHash(id.mChangeKey); + break; + } + default: + break; + } + d << ')'; + return debug; +} + +uint qHash(const EwsId &id, uint seed) +{ + return qHash(id.id(), seed) ^ qHash(id.changeKey(), seed) ^ static_cast(id.type()); +} + +#ifdef HAVE_INBOX_FILTERING_WORKAROUND +void EwsId::setInboxId(EwsId id) +{ + if (inboxId.isEmpty()) { + inboxId = id.mId; + } +} +#endif + diff --git a/resources/ews/ewsclient/ewsitem.h b/resources/ews/ewsclient/ewsitem.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsitem.h @@ -0,0 +1,61 @@ +/* + 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 EWSITEM_H +#define EWSITEM_H + +#include + +#include "ewsitembase.h" + +class QXmlStreamReader; +class QXmlStreamWriter; +class EwsItemPrivate; + +class EwsItem : public EwsItemBase +{ +public: + typedef QList List; + typedef QMultiMap HeaderMap; + + EwsItem(); + explicit EwsItem(QXmlStreamReader &reader); + EwsItem(const EwsItem &other); + EwsItem(EwsItem &&other); + virtual ~EwsItem(); + + EwsItem &operator=(const EwsItem &other); + EwsItem &operator=(EwsItem &&other); + + EwsItemType type() const; + void setType(EwsItemType type); + EwsItemType internalType() const; + + bool write(QXmlStreamWriter &writer) const; + + bool operator==(const EwsItem &other) const; +protected: + bool readBaseItemElement(QXmlStreamReader &reader); +}; + +Q_DECLARE_METATYPE(EwsItem) +Q_DECLARE_METATYPE(EwsItem::List) +Q_DECLARE_METATYPE(EwsItem::HeaderMap) + +#endif diff --git a/resources/ews/ewsclient/ewsitem.cpp b/resources/ews/ewsclient/ewsitem.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsitem.cpp @@ -0,0 +1,569 @@ +/* + 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 + +#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); + virtual 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")).arg(QStringLiteral("BodyType")); + return false; + } + bool isHtml; + if (bodyType == QStringLiteral("HTML")) { + isHtml = true; + } else if (bodyType == QStringLiteral("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() == QStringLiteral("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")); + 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() == QStringLiteral("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) +{ + QStringList categories = val.toStringList(); + + Q_FOREACH (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 + QStringRef elmName = reader.name(); + if (elmName == QStringLiteral("Item")) { + d->mType = EwsItemTypeItem; + QStringRef subtype = reader.attributes().value(QStringLiteral("xsi:type")); + if (!subtype.isEmpty()) { + auto tokens = subtype.split(QChar::fromLatin1(':')); + QStringRef type = tokens.size() == 1 ? tokens[0] : tokens[1]; + if (type == QStringLiteral("AbchPersonItemType")) { + d->mType = EwsItemTypeAbchPerson; + } + } + } else if (elmName == QStringLiteral("Message")) { + d->mType = EwsItemTypeMessage; + } else if (elmName == QStringLiteral("CalendarItem")) { + d->mType = EwsItemTypeCalendarItem; + } else if (elmName == QStringLiteral("Contact")) { + d->mType = EwsItemTypeContact; + } else if (elmName == QStringLiteral("DistributionList")) { + d->mType = EwsItemTypeDistributionList; + } else if (elmName == QStringLiteral("MeetingMessage")) { + d->mType = EwsItemTypeMeetingMessage; + } else if (elmName == QStringLiteral("MeetingRequest")) { + d->mType = EwsItemTypeMeetingRequest; + } else if (elmName == QStringLiteral("MeetingResponse")) { + d->mType = EwsItemTypeMeetingResponse; + } else if (elmName == QStringLiteral("MeetingCancellation")) { + d->mType = EwsItemTypeMeetingCancellation; + } else if (elmName == QStringLiteral("PostItem")) { + d->mType = EwsItemTypePostItem; + } else if (elmName == QStringLiteral("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/ewsitembase.h b/resources/ews/ewsclient/ewsitembase.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsitembase.h @@ -0,0 +1,58 @@ +/* + 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 EWSITEMBASE_H +#define EWSITEMBASE_H + +#include + +#include "ewsid.h" +#include "ewspropertyfield.h" + +class EwsItemBasePrivate; + +class EwsItemBase +{ +public: + EwsItemBase(const EwsItemBase &other); + EwsItemBase(EwsItemBase &&other); + virtual ~EwsItemBase(); + EwsItemBase &operator=(const EwsItemBase &other); + EwsItemBase &operator=(EwsItemBase &&other); + + bool isValid() const; + + bool hasField(EwsItemFields f) const; + QVariant operator[](EwsItemFields f) const; + + QVariant operator[](const EwsPropertyField &prop) const; + + void setField(EwsItemFields f, const QVariant &value); + void setProperty(const EwsPropertyField &prop, const QVariant &value); + +protected: + EwsItemBase(QSharedDataPointer priv); + + void resetFields(); + + QSharedDataPointer d; +}; + +#endif + diff --git a/resources/ews/ewsclient/ewsitembase.cpp b/resources/ews/ewsclient/ewsitembase.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsitembase.cpp @@ -0,0 +1,182 @@ +/* + 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 "ewsitembase.h" +#include "ewsitembase_p.h" + +#include "ewsclient_debug.h" + +EwsItemBasePrivate::EwsItemBasePrivate() + : mValid(false) +{ + qRegisterMetaType(); +} + +EwsItemBasePrivate::~EwsItemBasePrivate() +{ +} + +EwsItemBase::EwsItemBase(QSharedDataPointer priv) + : d(priv) +{ +} + +EwsItemBase::EwsItemBase(const EwsItemBase &other) + : d(other.d) +{ +} + +EwsItemBase::EwsItemBase(EwsItemBase &&other) + : d(std::move(other.d)) +{ +} + +EwsItemBase::~EwsItemBase() +{ +} + +bool EwsItemBase::isValid() const +{ + return d->mValid; +} + +bool EwsItemBasePrivate::extendedPropertyReader(QXmlStreamReader &reader, QVariant &val) +{ + EwsPropertyField prop; + QVariant value; + PropertyHash propHash = val.value(); + QString elmName = reader.name().toString(); + + while (reader.readNextStartElement()) { + if (reader.namespaceUri() != ewsTypeNsUri) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid namespace.") + .arg(elmName); + reader.skipCurrentElement(); + return false; + } + + if (reader.name() == QStringLiteral("FieldURI") + || reader.name() == QStringLiteral("IndexedFieldURI") + || reader.name() == QStringLiteral("ExtendedFieldURI")) { + if (!prop.read(reader)) { + return false; + reader.skipCurrentElement(); + } + reader.skipCurrentElement(); + } else if (reader.name() == QStringLiteral("Value")) { + value = reader.readElementText(); + } else if (reader.name() == QStringLiteral("Values")) { + QStringList values; + while (reader.readNextStartElement()) { + if (reader.namespaceUri() != ewsTypeNsUri) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid namespace.") + .arg(elmName); + reader.skipCurrentElement(); + reader.skipCurrentElement(); + return false; + } + + if (reader.name() == QStringLiteral("Value")) { + values.append(reader.readElementText()); + } + } + value = values; + } else { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - unexpected child element %2") + .arg(elmName).arg(reader.qualifiedName().toString()); + reader.skipCurrentElement(); + return false; + } + } + propHash.insert(prop, value); + + val = QVariant::fromValue(propHash); + + return true; +} + +bool EwsItemBasePrivate::extendedPropertyWriter(QXmlStreamWriter &writer, const QVariant &val) +{ + PropertyHash propHash = val.value(); + PropertyHash::const_iterator it; + for (it = propHash.cbegin(); it != propHash.cend();) { + /* EwsXml will already start the ExtendedProperty element expecting a single value. + * This is not exactly true for extended properties as there may be many. Work around this + * by avoiding writing the first element start tag and the last element end tag. */ + if (it != propHash.cbegin()) { + writer.writeStartElement(ewsTypeNsUri, QStringLiteral("ExtendedProperty")); + } + it.key().write(writer); + it.key().writeExtendedValue(writer, it.value()); + it++; + if (it != propHash.cend()) { + writer.writeEndElement(); + } + } + + return true; +} + +bool EwsItemBasePrivate::operator==(const EwsItemBasePrivate &other) const +{ + if (mValid != other.mValid) { + return false; + } + + return mFields == other.mFields; +} + +QVariant EwsItemBase::operator[](const EwsPropertyField &prop) const +{ + EwsItemBasePrivate::PropertyHash propHash = + d->mFields[EwsItemFieldExtendedProperties].value(); + EwsItemBasePrivate::PropertyHash::iterator it = propHash.find(prop); + if (it != propHash.end()) { + return it.value(); + } else { + return QVariant(); + } +} + +bool EwsItemBase::hasField(EwsItemFields f) const +{ + return d->mFields.contains(f); +} + +QVariant EwsItemBase::operator[](EwsItemFields f) const +{ + if (hasField(f)) { + return d->mFields[f]; + } else { + return QVariant(); + } +} + +void EwsItemBase::setField(EwsItemFields f, const QVariant &value) +{ + d->mFields[f] = value; +} + +void EwsItemBase::setProperty(const EwsPropertyField &prop, const QVariant &value) +{ + EwsItemBasePrivate::PropertyHash propHash = + d->mFields[EwsItemFieldExtendedProperties].value(); + propHash[prop] = value; + d->mFields[EwsItemFieldExtendedProperties] = QVariant::fromValue(propHash); +} diff --git a/resources/ews/ewsclient/ewsitembase_p.h b/resources/ews/ewsclient/ewsitembase_p.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsitembase_p.h @@ -0,0 +1,58 @@ +/* + 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 EWSITEMBASE_P_H +#define EWSITEMBASE_P_H + +#include + +#include "ewsid.h" + +class EwsItemBasePrivate : public QSharedData +{ +public: + typedef QHash PropertyHash; + + EwsItemBasePrivate(); + virtual ~EwsItemBasePrivate(); + + virtual EwsItemBasePrivate *clone() const = 0; + + static bool extendedPropertyReader(QXmlStreamReader &reader, QVariant &val); + static bool extendedPropertyWriter(QXmlStreamWriter &writer, const QVariant &val); + + // When the item, is first constructed it will only contain the id and will therefore be + // invalid. Once updated through EWS the remaining data will be populated and the item will + // be valid. + bool mValid; + + QHash mFields; + + bool operator==(const EwsItemBasePrivate &other) const; +}; + +template <> +Q_INLINE_TEMPLATE EwsItemBasePrivate *QSharedDataPointer::clone() +{ + return d->clone(); +} + +Q_DECLARE_METATYPE(EwsItemBasePrivate::PropertyHash) + +#endif diff --git a/resources/ews/ewsclient/ewsitemshape.h b/resources/ews/ewsclient/ewsitemshape.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsitemshape.h @@ -0,0 +1,85 @@ +/* + 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. +*/ + +#ifndef EWSITEMSHAPE_H +#define EWSITEMSHAPE_H + +#include + +#include "ewsfoldershape.h" + +class EwsItemShape : public EwsFolderShape +{ +public: + enum Flag { + IncludeMimeContent = 0x01, + FilterHtmlContent = 0x02, + ConvertHtmlToUtf8 = 0x04 + }; + enum BodyType { + BodyNone, + BodyBest, + BodyHtml, + BodyText + }; + + Q_DECLARE_FLAGS(Flags, Flag) + + explicit EwsItemShape(EwsBaseShape shape = EwsShapeDefault) : EwsFolderShape(shape), mBodyType(BodyNone) {}; + EwsItemShape(const EwsItemShape &other) + : EwsFolderShape(other), mBodyType(BodyNone) {}; + explicit EwsItemShape(EwsFolderShape &&other) + : EwsFolderShape(other), mBodyType(BodyNone) {}; + EwsItemShape &operator=(EwsItemShape &&other) + { + mBaseShape = other.mBaseShape; + mProps = std::move(other.mProps); + mFlags = other.mFlags; + mBodyType = other.mBodyType; + return *this; + } + EwsItemShape &operator=(const EwsItemShape &other) + { + mBaseShape = other.mBaseShape; + mProps = other.mProps; + mFlags = other.mFlags; + mBodyType = other.mBodyType; + return *this; + } + + void setFlags(Flags flags) + { + mFlags = flags; + } + + void setBodyType(BodyType type) + { + mBodyType = type; + } + + void write(QXmlStreamWriter &writer) const; +protected: + Flags mFlags; + BodyType mBodyType; + +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(EwsItemShape::Flags) + +#endif diff --git a/resources/ews/ewsclient/ewsitemshape.cpp b/resources/ews/ewsclient/ewsitemshape.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsitemshape.cpp @@ -0,0 +1,68 @@ +/* + 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 "ewsitemshape.h" + +void EwsItemShape::write(QXmlStreamWriter &writer) const +{ + writer.writeStartElement(ewsMsgNsUri, QStringLiteral("ItemShape")); + + // Write the base shape + writeBaseShape(writer); + + // Write the IncludeMimeContent element (if applicable) + if (mFlags.testFlag(IncludeMimeContent)) { + writer.writeTextElement(ewsTypeNsUri, QStringLiteral("IncludeMimeContent"), QStringLiteral("true")); + } + + // Write the BodyType element + if (mBodyType != BodyNone) { + QString bodyTypeText; + + switch (mBodyType) { + case BodyHtml: + bodyTypeText = QStringLiteral("HTML"); + break; + case BodyText: + bodyTypeText = QStringLiteral("Text"); + break; + default: + //case BodyBest: + bodyTypeText = QStringLiteral("Best"); + break; + } + writer.writeTextElement(ewsTypeNsUri, QStringLiteral("BodyType"), bodyTypeText); + } + + // Write the FilterHtmlContent element (if applicable) + if (mBodyType == BodyHtml && mFlags.testFlag(FilterHtmlContent)) { + writer.writeTextElement(ewsTypeNsUri, QStringLiteral("FilterHtmlContent"), QStringLiteral("true")); + } + + // Write the ConvertHtmlCodePageToUTF8 element (if applicable) + if (mBodyType == BodyHtml && mFlags.testFlag(ConvertHtmlToUtf8)) { + writer.writeTextElement(ewsTypeNsUri, QStringLiteral("ConvertHtmlCodePageToUTF8"), QStringLiteral("true")); + } + + // Write properties (if any) + writeProperties(writer); + + writer.writeEndElement(); +} + diff --git a/resources/ews/ewsclient/ewsjob.h b/resources/ews/ewsclient/ewsjob.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsjob.h @@ -0,0 +1,36 @@ +/* + 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. +*/ + +#ifndef EWSJOB_H +#define EWSJOB_H + +#include + +class EwsJob : public KCompositeJob +{ + Q_OBJECT +public: + explicit EwsJob(QObject *parent); + virtual ~EwsJob(); +protected: + virtual bool doKill() override; + bool setErrorMsg(const QString msg, int code = KJob::UserDefinedError); +}; + +#endif diff --git a/resources/ews/ewsclient/ewsjob.cpp b/resources/ews/ewsclient/ewsjob.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsjob.cpp @@ -0,0 +1,49 @@ +/* + 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 "ewsjob.h" +#include "ewsclient_debug.h" + +EwsJob::EwsJob(QObject *parent) + : KCompositeJob(parent) +{ +} + +EwsJob::~EwsJob() +{ +} + +bool EwsJob::doKill() +{ + Q_FOREACH (KJob *job, subjobs()) { + job->kill(KJob::Quietly); + } + clearSubjobs(); + + return true; +} + +bool EwsJob::setErrorMsg(const QString msg, int code) +{ + setError(code); + setErrorText(msg); + qCWarningNC(EWSCLI_LOG) << msg; + return false; +} + diff --git a/resources/ews/ewsclient/ewsmailbox.h b/resources/ews/ewsclient/ewsmailbox.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsmailbox.h @@ -0,0 +1,64 @@ +/* + 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 EWSMAILBOX_H +#define EWSMAILBOX_H + +#include +#include + +#include "ewsitembase.h" + +class EwsMailboxPrivate; + +namespace KMime +{ +namespace Types +{ +class Mailbox; +} +} + +class EwsMailbox +{ +public: + typedef QList List; + + EwsMailbox(); + explicit EwsMailbox(QXmlStreamReader &reader); + EwsMailbox(const EwsMailbox &other); + EwsMailbox(EwsMailbox &&other); + virtual ~EwsMailbox(); + + EwsMailbox &operator=(const EwsMailbox &other); + EwsMailbox &operator=(EwsMailbox &&other); + + bool isValid() const; + QString name() const; + QString email() const; + QString emailWithName() const; + operator KMime::Types::Mailbox() const; +protected: + QSharedDataPointer d; +}; + +Q_DECLARE_METATYPE(EwsMailbox) +Q_DECLARE_METATYPE(EwsMailbox::List) + +#endif diff --git a/resources/ews/ewsclient/ewsmailbox.cpp b/resources/ews/ewsclient/ewsmailbox.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsmailbox.cpp @@ -0,0 +1,145 @@ +/* + 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 "ewsmailbox.h" + +#include + +#include + +#include "ewsclient_debug.h" + +class EwsMailboxPrivate : public QSharedData +{ +public: + EwsMailboxPrivate(); + virtual ~EwsMailboxPrivate(); + + bool mValid; + + QString mName; + QString mEmail; +}; + +EwsMailboxPrivate::EwsMailboxPrivate() + : mValid(false) +{ +} + +EwsMailbox::EwsMailbox() + : d(new EwsMailboxPrivate()) +{ +} + +EwsMailbox::EwsMailbox(QXmlStreamReader &reader) + : d(new EwsMailboxPrivate()) +{ + while (reader.readNextStartElement()) { + if (reader.namespaceUri() != ewsTypeNsUri) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected namespace in mailbox element:") + << reader.namespaceUri(); + return; + } + + if (reader.name() == QStringLiteral("Name")) { + d->mName = reader.readElementText(); + if (reader.error() != QXmlStreamReader::NoError) { + qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read EWS request - invalid mailbox Name element."); + return; + } + } else if (reader.name() == QStringLiteral("EmailAddress")) { + d->mEmail = reader.readElementText(); + if (reader.error() != QXmlStreamReader::NoError) { + qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read EWS request - invalid mailbox EmailAddress element."); + return; + } + } else if (reader.name() == QStringLiteral("RoutingType") || + reader.name() == QStringLiteral("MailboxType") || + reader.name() == QStringLiteral("ItemId")) { + // Unsupported - ignore + //qCWarningNC(EWSCLIENT_LOG) << QStringLiteral("Unsupported mailbox element %1").arg(reader.name().toString()); + reader.skipCurrentElement(); + } + } + + d->mValid = true; +} + +EwsMailboxPrivate::~EwsMailboxPrivate() +{ +} + +EwsMailbox::EwsMailbox(const EwsMailbox &other) + : d(other.d) +{ +} + +EwsMailbox::EwsMailbox(EwsMailbox &&other) + : d(std::move(other.d)) +{ +} + +EwsMailbox::~EwsMailbox() +{ +} + +EwsMailbox &EwsMailbox::operator=(const EwsMailbox &other) +{ + d = other.d; + return *this; +} +EwsMailbox &EwsMailbox::operator=(EwsMailbox &&other) +{ + d = std::move(other.d); + return *this; +} + +bool EwsMailbox::isValid() const +{ + return d->mValid; +} + +QString EwsMailbox::name() const +{ + return d->mName; +} + +QString EwsMailbox::email() const +{ + return d->mEmail; +} + +QString EwsMailbox::emailWithName() const +{ + if (d->mName.isEmpty()) { + return d->mEmail; + } else { + return QStringLiteral("%1 <%2>").arg(d->mName).arg(d->mEmail); + } +} + +EwsMailbox::operator KMime::Types::Mailbox() const +{ + KMime::Types::Mailbox mbox; + mbox.setAddress(d->mEmail.toAscii()); + if (!d->mName.isEmpty()) { + mbox.setName(d->mName); + } + return mbox; +} diff --git a/resources/ews/ewsclient/ewsmovefolderrequest.h b/resources/ews/ewsclient/ewsmovefolderrequest.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsmovefolderrequest.h @@ -0,0 +1,79 @@ +/* + 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 EWSMOVEFOLDERREQUEST_H +#define EWSMOVEFOLDERREQUEST_H + +#include +#include + +#include "ewsfolder.h" +#include "ewsrequest.h" +#include "ewstypes.h" + +class QXmlStreamReader; +class QXmlStreamWriter; + +class EwsMoveFolderRequest : public EwsRequest +{ + Q_OBJECT +public: + class Response : public EwsRequest::Response + { + public: + const EwsId &folderId() const + { + return mId; + }; + protected: + Response(QXmlStreamReader &reader); + + EwsId mId; + + friend class EwsMoveFolderRequest; + }; + + EwsMoveFolderRequest(EwsClient &client, QObject *parent); + virtual ~EwsMoveFolderRequest(); + + void setFolderIds(const EwsId::List &ids) + { + mIds = ids; + }; + void setDestinationFolderId(const EwsId &id) + { + mDestFolderId = id; + }; + + virtual void start() override; + + const QList &responses() const + { + return mResponses; + }; +protected: + virtual bool parseResult(QXmlStreamReader &reader) override; + bool parseItemsResponse(QXmlStreamReader &reader); +private: + EwsId::List mIds; + EwsId mDestFolderId; + QList mResponses; +}; + +#endif diff --git a/resources/ews/ewsclient/ewsmovefolderrequest.cpp b/resources/ews/ewsclient/ewsmovefolderrequest.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsmovefolderrequest.cpp @@ -0,0 +1,120 @@ +/* + 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 "ewsmovefolderrequest.h" +#include "ewsclient_debug.h" + +EwsMoveFolderRequest::EwsMoveFolderRequest(EwsClient &client, QObject *parent) + : EwsRequest(client, parent) +{ +} + +EwsMoveFolderRequest::~EwsMoveFolderRequest() +{ +} + +void EwsMoveFolderRequest::start() +{ + QString reqString; + QXmlStreamWriter writer(&reqString); + + startSoapDocument(writer); + + writer.writeStartElement(ewsMsgNsUri, QStringLiteral("MoveFolder")); + + writer.writeStartElement(ewsMsgNsUri, QStringLiteral("ToFolderId")); + mDestFolderId.writeFolderIds(writer); + writer.writeEndElement(); + + writer.writeStartElement(ewsMsgNsUri, QStringLiteral("FolderIds")); + Q_FOREACH (const EwsId &id, mIds) { + id.writeFolderIds(writer); + } + writer.writeEndElement(); + + writer.writeEndElement(); + + endSoapDocument(writer); + + qCDebugNC(EWSCLI_REQUEST_LOG) << QStringLiteral("Starting MoveFolder request (%1 folders, to %2)") + .arg(mIds.size()).arg(mDestFolderId.id()); + + qCDebug(EWSCLI_PROTO_LOG) << reqString; + + prepare(reqString); + + doSend(); +} + +bool EwsMoveFolderRequest::parseResult(QXmlStreamReader &reader) +{ + return parseResponseMessage(reader, QStringLiteral("MoveFolder"), + [this](QXmlStreamReader &reader) {return parseItemsResponse(reader);}); +} + +bool EwsMoveFolderRequest::parseItemsResponse(QXmlStreamReader &reader) +{ + Response resp(reader); + if (resp.responseClass() == EwsResponseUnknown) { + return false; + } + + if (EWSCLI_REQUEST_LOG().isDebugEnabled()) { + if (resp.isSuccess()) { + qCDebug(EWSCLI_REQUEST_LOG) << QStringLiteral("Got MoveFolder response - OK"); + } else { + qCDebug(EWSCLI_REQUEST_LOG) << QStringLiteral("Got MoveFolder response - %1") + .arg(resp.responseMessage()); + } + } + mResponses.append(resp); + return true; +} + +EwsMoveFolderRequest::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())); + return; + } + + if (reader.name() == QStringLiteral("Folders")) { + if (reader.readNextStartElement()) { + EwsFolder folder(reader); + if (!folder.isValid()) { + return; + } + mId = folder[EwsFolderFieldFolderId].value(); + + // Finish the Folders element. + reader.skipCurrentElement(); + } + } else if (!readResponseElement(reader)) { + setErrorMsg(QStringLiteral("Failed to read EWS request - invalid response element.")); + return; + } + } +} diff --git a/resources/ews/ewsclient/ewsmoveitemrequest.h b/resources/ews/ewsclient/ewsmoveitemrequest.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsmoveitemrequest.h @@ -0,0 +1,79 @@ +/* + 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 EWSMOVEITEMREQUEST_H +#define EWSMOVEITEMREQUEST_H + +#include +#include + +#include "ewsitem.h" +#include "ewsrequest.h" +#include "ewstypes.h" + +class QXmlStreamReader; +class QXmlStreamWriter; + +class EwsMoveItemRequest : public EwsRequest +{ + Q_OBJECT +public: + class Response : public EwsRequest::Response + { + public: + const EwsId &itemId() const + { + return mId; + }; + protected: + Response(QXmlStreamReader &reader); + + EwsId mId; + + friend class EwsMoveItemRequest; + }; + + EwsMoveItemRequest(EwsClient &client, QObject *parent); + virtual ~EwsMoveItemRequest(); + + void setItemIds(const EwsId::List &ids) + { + mIds = ids; + }; + void setDestinationFolderId(const EwsId &id) + { + mDestFolderId = id; + }; + + virtual void start() override; + + const QList &responses() const + { + return mResponses; + }; +protected: + virtual bool parseResult(QXmlStreamReader &reader) override; + bool parseItemsResponse(QXmlStreamReader &reader); +private: + EwsId::List mIds; + EwsId mDestFolderId; + QList mResponses; +}; + +#endif diff --git a/resources/ews/ewsclient/ewsmoveitemrequest.cpp b/resources/ews/ewsclient/ewsmoveitemrequest.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsmoveitemrequest.cpp @@ -0,0 +1,119 @@ +/* + 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")); + Q_FOREACH (const EwsId &id, 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())); + return; + } + + if (reader.name() == QStringLiteral("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.h b/resources/ews/ewsclient/ewsoccurrence.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsoccurrence.h @@ -0,0 +1,59 @@ +/* + 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 EWSOCCURRENCE_H +#define EWSOCCURRENCE_H + +#include +#include +#include + +#include "ewstypes.h" + +class EwsOccurrencePrivate; +class EwsId; +class QXmlStreamReader; + +class EwsOccurrence +{ +public: + typedef QList List; + + EwsOccurrence(); + explicit EwsOccurrence(QXmlStreamReader &reader); + EwsOccurrence(const EwsOccurrence &other); + EwsOccurrence(EwsOccurrence &&other); + virtual ~EwsOccurrence(); + + EwsOccurrence &operator=(const EwsOccurrence &other); + EwsOccurrence &operator=(EwsOccurrence &&other); + + bool isValid() const; + const EwsId &itemId() const; + QDateTime start() const; + QDateTime end() const; + QDateTime originalStart() const; +protected: + QSharedDataPointer d; +}; + +Q_DECLARE_METATYPE(EwsOccurrence) +Q_DECLARE_METATYPE(EwsOccurrence::List) + +#endif diff --git a/resources/ews/ewsclient/ewsoccurrence.cpp b/resources/ews/ewsclient/ewsoccurrence.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsoccurrence.cpp @@ -0,0 +1,149 @@ +/* + 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() == QStringLiteral("ItemId")) { + d->mItemId = EwsId(reader); + reader.skipCurrentElement(); + } else if (reader.name() == QStringLiteral("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")); + return; + } + } else if (reader.name() == QStringLiteral("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")); + return; + } + } else if (reader.name() == QStringLiteral("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")); + return; + } + } else { + qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - unknown element: %2.") + .arg(QStringLiteral("Occurrence")).arg(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.h b/resources/ews/ewsclient/ewspoxautodiscoverrequest.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewspoxautodiscoverrequest.h @@ -0,0 +1,140 @@ +/* + 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 EWSPOXAUTODISCOVERREQUEST_H +#define EWSPOXAUTODISCOVERREQUEST_H + +#include +#include + +#include "ewsjob.h" +#include "ewsserverversion.h" +#include "ewstypes.h" + +namespace KIO +{ +class Job; +} + +class EwsPoxAutodiscoverRequest : public EwsJob +{ + Q_OBJECT +public: + enum Action { + RedirectUrl = 0, + RedirectAddr, + Settings, + }; + + enum ProtocolType { + ExchangeProto, + ExchangeProxyProto, + ExchangeWebProto, + UnknownProto + }; + + class Protocol + { + public: + Protocol() : mType(UnknownProto) {}; + bool isValid() const + { + return mType != UnknownProto; + }; + ProtocolType type() const + { + return mType; + }; + const QString &ewsUrl() const + { + return mEwsUrl; + }; + const QString &oabUrl() const + { + return mOabUrl; + }; + private: + ProtocolType mType; + QString mEwsUrl; + QString mOabUrl; + friend class EwsPoxAutodiscoverRequest; + }; + + EwsPoxAutodiscoverRequest(const QUrl &url, const QString &email, const QString &userAgent, + bool useNTLMv2, QObject *parent); + virtual ~EwsPoxAutodiscoverRequest(); + + const EwsServerVersion &serverVersion() const + { + return mServerVersion; + }; + + void dump() const; + + virtual void start() override; + + Action action() const + { + return mAction; + }; + const Protocol protocol(ProtocolType type) const + { + return mProtocols.value(type); + }; + const QString &redirectAddr() const + { + return mRedirectAddr; + }; + const QString &redirectUrl() const + { + return mRedirectUrl; + }; + const QUrl &lastHttpUrl() const + { + return mLastUrl; + }; + +protected: + void doSend(); + void prepare(const QString body); + bool readResponse(QXmlStreamReader &reader); + +protected Q_SLOTS: + void requestResult(KJob *job); + void requestData(KIO::Job *job, const QByteArray &data); + void requestRedirect(KIO::Job *job, const QUrl &url); +private: + bool readAccount(QXmlStreamReader &reader); + bool readProtocol(QXmlStreamReader &reader); + + QString mResponseData; + QString mBody; + QUrl mUrl; + QString mEmail; + QString mUserAgent; + bool mUseNTLMv2; + EwsServerVersion mServerVersion; + Action mAction; + QString mRedirectUrl; + QString mRedirectAddr; + QHash mProtocols; + QUrl mLastUrl; +}; + +#endif diff --git a/resources/ews/ewsclient/ewspoxautodiscoverrequest.cpp b/resources/ews/ewsclient/ewspoxautodiscoverrequest.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewspoxautodiscoverrequest.cpp @@ -0,0 +1,284 @@ +/* + 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() != QStringLiteral("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() != QStringLiteral("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() == QStringLiteral("User")) { + reader.skipCurrentElement(); + } else if (reader.name() == QStringLiteral("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"))); + } + } + return true; +} + +bool EwsPoxAutodiscoverRequest::readAccount(QXmlStreamReader &reader) +{ + while (reader.readNextStartElement()) { + if (reader.namespaceUri() != poxAdOuRespNsUri) { + return setErrorMsg(QStringLiteral("Failed to read POX response - invalid namespace")); + } + + if (reader.name() == QStringLiteral("Action")) { + QString action = reader.readElementText(); + if (action == QStringLiteral("settings")) { + mAction = Settings; + } else if (action == QStringLiteral("redirectUrl")) { + mAction = RedirectUrl; + } else if (action == QStringLiteral("redirectAddr")) { + mAction = RedirectAddr; + } else { + return setErrorMsg(QStringLiteral("Failed to read POX response - unknown action '%1'") + .arg(action)); + } + } else if (reader.name() == QStringLiteral("RedirectUrl")) { + mRedirectUrl = reader.readElementText(); + } else if (reader.name() == QStringLiteral("RedirectAddr")) { + mRedirectAddr = reader.readElementText(); + } else if (reader.name() == QStringLiteral("RedirectAddr")) { + mRedirectAddr = reader.readElementText(); + } else if (reader.name() == QStringLiteral("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")); + } + + if (reader.name() == QStringLiteral("Type")) { + QString type = reader.readElementText(); + if (type == QStringLiteral("EXCH")) { + proto.mType = ExchangeProto; + } else if (type == QStringLiteral("EXPR")) { + proto.mType = ExchangeProxyProto; + } else if (type == QStringLiteral("WEB")) { + proto.mType = ExchangeWebProto; + } else { + return setErrorMsg(QStringLiteral("Failed to read POX response - unknown protocol '%1'") + .arg(type)); + } + } else if (reader.name() == QStringLiteral("EwsUrl")) { + proto.mEwsUrl = reader.readElementText(); + } else if (reader.name() == QStringLiteral("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/ewspropertyfield.h b/resources/ews/ewsclient/ewspropertyfield.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewspropertyfield.h @@ -0,0 +1,77 @@ +/* + 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 EWSPROPERTYFIELD_H +#define EWSPROPERTYFIELD_H + +#include +#include + +#include "ewstypes.h" + +class EwsPropertyFieldPrivate; + +class EwsPropertyField +{ +public: + enum Type { + Field, + ExtendedField, + IndexedField, + UnknownField + }; + + EwsPropertyField(); + explicit EwsPropertyField(const QString &uri); // FieldURI + EwsPropertyField(const QString &uri, unsigned index); // IndexedFieldURI + EwsPropertyField(EwsDistinguishedPropSetId psid, unsigned id, EwsPropertyType type); + EwsPropertyField(EwsDistinguishedPropSetId psid, const QString &name, EwsPropertyType type); + EwsPropertyField(const QString &psid, unsigned id, EwsPropertyType type); + EwsPropertyField(const QString &psid, const QString &name, EwsPropertyType type); + EwsPropertyField(unsigned tag, EwsPropertyType type); + EwsPropertyField(const EwsPropertyField &other); + ~EwsPropertyField(); + + EwsPropertyField &operator=(const EwsPropertyField &other); + bool operator==(const EwsPropertyField &other) const; + + EwsPropertyField(EwsPropertyField &&other); + EwsPropertyField &operator=(EwsPropertyField &&other); + + void write(QXmlStreamWriter &writer) const; + bool read(QXmlStreamReader &reader); + + bool writeWithValue(QXmlStreamWriter &writer, const QVariant &value) const; + void writeValue(QXmlStreamWriter &writer, const QVariant &value) const; + void writeExtendedValue(QXmlStreamWriter &writer, const QVariant &value) const; + + Type type() const; + QString uri() const; +private: + QSharedDataPointer d; + + friend uint qHash(const EwsPropertyField &prop, uint seed); + friend QDebug operator<<(QDebug debug, const EwsPropertyField &prop); +}; + +uint qHash(const EwsPropertyField &prop, uint seed); + +QDebug operator<<(QDebug debug, const EwsPropertyField &prop); + +#endif diff --git a/resources/ews/ewsclient/ewspropertyfield.cpp b/resources/ews/ewsclient/ewspropertyfield.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewspropertyfield.cpp @@ -0,0 +1,636 @@ +/* + 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 "ewspropertyfield.h" + +#include + +#include "ewsclient_debug.h" + +static const QString distinguishedPropSetIdNames[] = { + QStringLiteral("Meeting"), + QStringLiteral("Appointment"), + QStringLiteral("Common"), + QStringLiteral("PublicStrings"), + QStringLiteral("Address"), + QStringLiteral("InternetHeaders"), + QStringLiteral("CalendarAssistant"), + QStringLiteral("UnifiedMessaging") +}; + +static const QString propertyTypeNames[] = { + QStringLiteral("ApplicationTime"), + QStringLiteral("ApplicationTimeArray"), + QStringLiteral("Binary"), + QStringLiteral("BinaryArray"), + QStringLiteral("Boolean"), + QStringLiteral("CLSID"), + QStringLiteral("CLSIDArray"), + QStringLiteral("Currency"), + QStringLiteral("CurrencyArray"), + QStringLiteral("Double"), + QStringLiteral("DoubleArray"), + QStringLiteral("Error"), + QStringLiteral("Float"), + QStringLiteral("FloatArray"), + QStringLiteral("Integer"), + QStringLiteral("IntegerArray"), + QStringLiteral("Long"), + QStringLiteral("LongArray"), + QStringLiteral("Null"), + QStringLiteral("Object"), + QStringLiteral("ObjectArray"), + QStringLiteral("Short"), + QStringLiteral("ShortArray"), + QStringLiteral("SystemTime"), + QStringLiteral("SystemTimeArray"), + QStringLiteral("String"), + QStringLiteral("StringArray") +}; + +class EwsPropertyFieldPrivate : public QSharedData +{ +public: + EwsPropertyFieldPrivate() + : mPropType(EwsPropertyField::UnknownField), mIndex(0), mPsIdType(DistinguishedPropSet), + mPsDid(EwsPropSetMeeting), mIdType(PropName), mId(0), mHasTag(false), mTag(0), + mType(EwsPropTypeNull), mHash(0) + {}; + + enum PropSetIdType { + DistinguishedPropSet, + RealPropSet + }; + + enum PropIdType { + PropName, + PropId + }; + + EwsPropertyField::Type mPropType; + + QString mUri; + unsigned mIndex; + + PropSetIdType mPsIdType; + EwsDistinguishedPropSetId mPsDid; + QString mPsId; + + PropIdType mIdType; + unsigned mId; + QString mName; + + bool mHasTag; + unsigned mTag; + + EwsPropertyType mType; + + uint mHash; // Precalculated hash for the qHash() function. + + void recalcHash(); +}; + +void EwsPropertyFieldPrivate::recalcHash() +{ + mHash = 0; + switch (mPropType) { + case EwsPropertyField::Field: + mHash = 0x00000000 | (qHash(mUri) & 0x3FFFFFFF); + break; + case EwsPropertyField::IndexedField: + mHash = 0x80000000 | ((qHash(mUri) ^ mIndex) & 0x3FFFFFFF); + break; + case EwsPropertyField::ExtendedField: + if (mHasTag) { + mHash = 0x40000000 | mTag; + } else { + if (mPsIdType == DistinguishedPropSet) { + mHash |= mPsDid << 16; + } else { + mHash |= (qHash(mPsId) & 0x1FFF) << 16; + } + + if (mIdType == PropId) { + mHash |= mId & 0xFFFF; + } else { + mHash |= (qHash(mName) & 0xFFFF); + } + mHash |= 0xC0000000; + } + break; + default: + break; + } +} + +EwsPropertyField::EwsPropertyField() + : d(new EwsPropertyFieldPrivate()) +{ +} + +EwsPropertyField::EwsPropertyField(const QString &uri) + : d(new EwsPropertyFieldPrivate()) +{ + d->mPropType = Field; + d->mUri = uri; + d->recalcHash(); +} + +EwsPropertyField::EwsPropertyField(const QString &uri, unsigned index) + : d(new EwsPropertyFieldPrivate()) +{ + d->mPropType = IndexedField; + d->mUri = uri; + d->mIndex = index; + d->recalcHash(); +} + +EwsPropertyField::EwsPropertyField(EwsDistinguishedPropSetId psid, unsigned id, EwsPropertyType type) + : d(new EwsPropertyFieldPrivate()) +{ + d->mPropType = ExtendedField; + + d->mPsIdType = EwsPropertyFieldPrivate::DistinguishedPropSet; + d->mPsDid = psid; + + d->mIdType = EwsPropertyFieldPrivate::PropId; + d->mId = id; + + d->mType = type; + d->recalcHash(); +} + +EwsPropertyField::EwsPropertyField(EwsDistinguishedPropSetId psid, const QString &name, EwsPropertyType type) + : d(new EwsPropertyFieldPrivate()) +{ + d->mPropType = ExtendedField; + + d->mPsIdType = EwsPropertyFieldPrivate::DistinguishedPropSet; + d->mPsDid = psid; + + d->mIdType = EwsPropertyFieldPrivate::PropName; + d->mName = name; + + d->mType = type; + d->recalcHash(); +} + +EwsPropertyField::EwsPropertyField(const QString &psid, unsigned id, EwsPropertyType type) + : d(new EwsPropertyFieldPrivate()) +{ + d->mPropType = ExtendedField; + + d->mPsIdType = EwsPropertyFieldPrivate::RealPropSet; + d->mPsId = psid; + + d->mIdType = EwsPropertyFieldPrivate::PropId; + d->mId = id; + + d->mType = type; + d->recalcHash(); +} + +EwsPropertyField::EwsPropertyField(const QString &psid, const QString &name, EwsPropertyType type) + : d(new EwsPropertyFieldPrivate()) +{ + d->mPropType = ExtendedField; + + d->mPsIdType = EwsPropertyFieldPrivate::RealPropSet; + d->mPsId = psid; + + d->mIdType = EwsPropertyFieldPrivate::PropName; + d->mName = name; + + d->mType = type; + d->recalcHash(); +} + +EwsPropertyField::EwsPropertyField(unsigned tag, EwsPropertyType type) + : d(new EwsPropertyFieldPrivate()) +{ + d->mPropType = ExtendedField; + + d->mHasTag = true; + d->mTag = tag; + + d->mType = type; + d->recalcHash(); +} + +EwsPropertyField::EwsPropertyField(const EwsPropertyField &other) + : d(other.d) +{ +} + +EwsPropertyField::EwsPropertyField(EwsPropertyField &&other) + : d(other.d) +{ +} + +EwsPropertyField &EwsPropertyField::operator=(EwsPropertyField &&other) +{ + d = other.d; + return *this; +} + +EwsPropertyField::~EwsPropertyField() +{ +} + +EwsPropertyField &EwsPropertyField::operator=(const EwsPropertyField &other) +{ + d = other.d; + return *this; +} + +bool EwsPropertyField::operator==(const EwsPropertyField &other) const +{ + if (d == other.d) + return true; + + const EwsPropertyFieldPrivate *od = other.d; + + if (d->mPropType != od->mPropType) + return false; + + switch (d->mPropType) { + case UnknownField: + return true; + case Field: + return (d->mUri == od->mUri); + case IndexedField: + return (d->mUri == od->mUri) && (d->mIndex == od->mIndex); + case ExtendedField: + if (d->mType != od->mType) + return false; + + if (d->mHasTag != od->mHasTag) + return false; + else if (d->mHasTag) + return d->mTag == od->mTag; + + if (d->mPsIdType != od->mPsIdType) + return false; + else if ((d->mPsIdType == EwsPropertyFieldPrivate::DistinguishedPropSet) + && (d->mPsDid != od->mPsDid)) + return false; + else if ((d->mPsIdType == EwsPropertyFieldPrivate::RealPropSet) && (d->mPsId != od->mPsId)) + return false; + + if (d->mIdType != od->mIdType) + return false; + else if ((d->mIdType == EwsPropertyFieldPrivate::PropId) && (d->mId != od->mId)) + return false; + else if ((d->mIdType == EwsPropertyFieldPrivate::PropName) && (d->mName != od->mName)) + return false; + return true; + default: + return false; + } + + // Shouldn't get here. + return false; +} + +void EwsPropertyField::write(QXmlStreamWriter &writer) const +{ + switch (d->mPropType) { + case Field: + writer.writeStartElement(ewsTypeNsUri, QStringLiteral("FieldURI")); + writer.writeAttribute(QStringLiteral("FieldURI"), d->mUri); + writer.writeEndElement(); + break; + case IndexedField: { + writer.writeStartElement(ewsTypeNsUri, QStringLiteral("IndexedFieldURI")); + writer.writeAttribute(QStringLiteral("FieldURI"), d->mUri); + QStringList tokens = d->mUri.split(QChar::fromLatin1(':')); + writer.writeAttribute(QStringLiteral("FieldIndex"), tokens[1] + QString::number(d->mIndex)); + writer.writeEndElement(); + break; + } + case ExtendedField: + writer.writeStartElement(ewsTypeNsUri, QStringLiteral("ExtendedFieldURI")); + if (d->mHasTag) { + writer.writeAttribute(QStringLiteral("PropertyTag"), + QStringLiteral("0x") + QString::number(d->mTag, 16)); + } else { + if (d->mPsIdType == EwsPropertyFieldPrivate::DistinguishedPropSet) { + writer.writeAttribute(QStringLiteral("DistinguishedPropertySetId"), + distinguishedPropSetIdNames[d->mPsDid]); + } else { + writer.writeAttribute(QStringLiteral("PropertySetId"), d->mPsId); + } + + if (d->mIdType == EwsPropertyFieldPrivate::PropId) { + writer.writeAttribute(QStringLiteral("PropertyId"), QString::number(d->mId)); + } else { + writer.writeAttribute(QStringLiteral("PropertyName"), d->mName); + } + } + writer.writeAttribute(QStringLiteral("PropertyType"), propertyTypeNames[d->mType]); + writer.writeEndElement(); + break; + case UnknownField: + break; + } +} + +bool EwsPropertyField::read(QXmlStreamReader &reader) +{ + if (reader.namespaceUri() != ewsTypeNsUri) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading property field - invalid namespace."); + return false; + } + + QXmlStreamAttributes attrs = reader.attributes(); + bool ok; + + // First check the property type + if (reader.name() == QStringLiteral("FieldURI")) { + if (!attrs.hasAttribute(QStringLiteral("FieldURI"))) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading property field - missing %1 attribute.") + .arg(QStringLiteral("FieldURI")); + return false; + } + d->mPropType = Field; + d->mUri = attrs.value(QStringLiteral("FieldURI")).toString(); + } else if (reader.name() == QStringLiteral("IndexedFieldURI")) { + if (!attrs.hasAttribute(QStringLiteral("FieldURI"))) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading property field - missing %1 attribute.") + .arg(QStringLiteral("FieldURI")); + return false; + } + if (!attrs.hasAttribute(QStringLiteral("FieldIndex"))) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading property field - missing %1 attribute.") + .arg(QStringLiteral("FieldIndex")); + return false; + } + QString uri = attrs.value(QStringLiteral("FieldURI")).toString(); + QStringList tokens = uri.split(QChar::fromLatin1(':')); + QString indexStr = attrs.value(QStringLiteral("FieldIndex")).toString(); + if (!indexStr.startsWith(tokens[1])) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading property field - malformed %1 attribute.") + .arg(QStringLiteral("FieldIndex")); + return false; + } + unsigned index = indexStr.mid(tokens[1].size()).toUInt(&ok, 0); + if (!ok) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading property field - error reading %1 attribute.") + .arg(QStringLiteral("FieldIndex")); + return false; + } + d->mPropType = IndexedField; + d->mUri = uri; + d->mIndex = index; + } else if (reader.name() == QStringLiteral("ExtendedFieldURI")) { + if (!attrs.hasAttribute(QStringLiteral("PropertyType"))) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading property field - missing %1 attribute.") + .arg(QStringLiteral("PropertyType")); + return false; + } + QStringRef propTypeText = attrs.value(QStringLiteral("PropertyType")); + unsigned i; + EwsPropertyType propType; + for (i = 0; i < sizeof(propertyTypeNames) / sizeof(propertyTypeNames[0]); ++i) { + if (propTypeText == propertyTypeNames[i]) { + propType = static_cast(i); + break; + } + } + if (i == sizeof(propertyTypeNames) / sizeof(propertyTypeNames[0])) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading property field - error reading %1 attribute.") + .arg(QStringLiteral("PropertyType")); + return false; + } + + if (attrs.hasAttribute(QStringLiteral("PropertyTag"))) { + + unsigned tag = attrs.value(QStringLiteral("PropertyTag")).toUInt(&ok, 0); + if (!ok) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading property field - error reading %1 attribute.") + .arg(QStringLiteral("PropertyTag")); + return false; + } + d->mHasTag = true; + d->mTag = tag; + } else { + EwsPropertyFieldPrivate::PropSetIdType psIdType = EwsPropertyFieldPrivate::DistinguishedPropSet; + EwsDistinguishedPropSetId psDid = EwsPropSetMeeting; + QString psId; + + EwsPropertyFieldPrivate::PropIdType idType = EwsPropertyFieldPrivate::PropName; + unsigned id = 0; + QString name; + if (attrs.hasAttribute(QStringLiteral("PropertyId"))) { + id = attrs.value(QStringLiteral("PropertyId")).toUInt(&ok, 0); + if (!ok) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading property field - error reading %1 attribute.") + .arg(QStringLiteral("PropertyId")); + return false; + } + idType = EwsPropertyFieldPrivate::PropId; + } else if (attrs.hasAttribute(QStringLiteral("PropertyName"))) { + name = attrs.value(QStringLiteral("PropertyName")).toString(); + idType = EwsPropertyFieldPrivate::PropName; + } else { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading property field - missing one of %1 or %2 attributes.") + .arg(QStringLiteral("PropertyId").arg(QStringLiteral("PropertyName"))); + return false; + } + + if (attrs.hasAttribute(QStringLiteral("DistinguishedPropertySetId"))) { + QStringRef didText = attrs.value(QStringLiteral("DistinguishedPropertySetId")); + unsigned i; + for (i = 0; i < sizeof(distinguishedPropSetIdNames) / sizeof(distinguishedPropSetIdNames[0]); ++i) { + if (didText == distinguishedPropSetIdNames[i]) { + psDid = static_cast(i); + break; + } + } + if (i == sizeof(distinguishedPropSetIdNames) / sizeof(distinguishedPropSetIdNames[0])) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading property field - error reading %1 attribute.") + .arg(QStringLiteral("DistinguishedPropertySetId")); + return false; + } + psIdType = EwsPropertyFieldPrivate::DistinguishedPropSet; + } else if (attrs.hasAttribute(QStringLiteral("PropertySetId"))) { + psId = attrs.value(QStringLiteral("PropertySetId")).toString(); + psIdType = EwsPropertyFieldPrivate::RealPropSet; + } else { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading property field - missing one of %1 or %2 attributes.") + .arg(QStringLiteral("DistinguishedPropertySetId").arg(QStringLiteral("PropertySetId"))); + return false; + } + d->mPsIdType = psIdType; + d->mPsDid = psDid; + d->mPsId = psId; + d->mIdType = idType; + d->mId = id; + d->mName = name; + } + + d->mType = propType; + d->mPropType = ExtendedField; + } + d->recalcHash(); + return true; +} + +uint qHash(const EwsPropertyField &prop, uint seed) +{ + return prop.d->mHasTag ^ seed; +} + +QDebug operator<<(QDebug debug, const EwsPropertyField &prop) +{ + QDebugStateSaver saver(debug); + QDebug d = debug.nospace().noquote(); + d << QStringLiteral("EwsPropertyField("); + + switch (prop.d->mPropType) { + case EwsPropertyField::Field: + d << QStringLiteral("FieldUri: ") << prop.d->mUri; + break; + case EwsPropertyField::IndexedField: + d << QStringLiteral("IndexedFieldUri: ") << prop.d->mUri << '@' << prop.d->mIndex; + break; + case EwsPropertyField::ExtendedField: + d << QStringLiteral("ExtendedFieldUri: "); + if (prop.d->mHasTag) { + d << QStringLiteral("tag: 0x") << QString::number(prop.d->mTag, 16); + } else { + if (prop.d->mPsIdType == EwsPropertyFieldPrivate::DistinguishedPropSet) { + d << QStringLiteral("psdid: ") << distinguishedPropSetIdNames[prop.d->mPsDid]; + } else { + d << QStringLiteral("psid: ") << prop.d->mPsId; + } + d << QStringLiteral(", "); + + if (prop.d->mIdType == EwsPropertyFieldPrivate::PropId) { + d << QStringLiteral("id: 0x") << QString::number(prop.d->mId, 16); + } else { + d << QStringLiteral("name: ") << prop.d->mName; + } + } + d << QStringLiteral(", type: ") << propertyTypeNames[prop.d->mType]; + break; + case EwsPropertyField::UnknownField: + d << QStringLiteral("Unknown"); + break; + } + d << ')'; + return debug; +} + +bool EwsPropertyField::writeWithValue(QXmlStreamWriter &writer, const QVariant &value) const +{ + switch (d->mPropType) { + case Field: { + QStringList tokens = d->mUri.split(QChar::fromLatin1(':')); + if (tokens.size() != 2) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Invalid field URI: %1").arg(d->mUri); + return false; + } + writer.writeStartElement(ewsTypeNsUri, tokens[1]); + writeValue(writer, value); + writer.writeEndElement(); + break; + } + case IndexedField: { + QStringList tokens = d->mUri.split(QChar::fromLatin1(':')); + if (tokens.size() != 2) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Invalid field URI: %1").arg(d->mUri); + return false; + } + writer.writeStartElement(ewsTypeNsUri, tokens[1] + QStringLiteral("es")); + writer.writeStartElement(ewsTypeNsUri, QStringLiteral("Entry")); + writer.writeAttribute(QStringLiteral("Key"), tokens[1] + QString::number(d->mIndex)); + writeValue(writer, value); + writer.writeEndElement(); + writer.writeEndElement(); + break; + } + case ExtendedField: + writer.writeStartElement(ewsTypeNsUri, QStringLiteral("ExtendedProperty")); + write(writer); + writeExtendedValue(writer, value); + writer.writeEndElement(); + break; + default: + return false; + } + + return true; +} + +void EwsPropertyField::writeValue(QXmlStreamWriter &writer, const QVariant &value) const +{ + switch (value.type()) { + case QVariant::StringList: { + QStringList list = value.toStringList(); + Q_FOREACH (const QString &str, list) { + writer.writeTextElement(ewsTypeNsUri, QStringLiteral("String"), str); + } + break; + } + case QVariant::String: + writer.writeCharacters(value.toString()); + break; + default: + qCWarning(EWSCLI_LOG) << QStringLiteral("Unknown variant type to write: %1") + .arg(QString::fromLatin1(value.typeName())); + } +} + +void EwsPropertyField::writeExtendedValue(QXmlStreamWriter &writer, const QVariant &value) const +{ + switch (value.type()) { + case QVariant::StringList: { + QStringList list = value.toStringList(); + writer.writeStartElement(ewsTypeNsUri, QStringLiteral("Values")); + Q_FOREACH (const QString &str, list) { + writer.writeTextElement(ewsTypeNsUri, QStringLiteral("Value"), str); + } + writer.writeEndElement(); + break; + } + case QVariant::String: + writer.writeStartElement(ewsTypeNsUri, QStringLiteral("Value")); + writer.writeCharacters(value.toString()); + writer.writeEndElement(); + break; + default: + qCWarning(EWSCLI_LOG) << QStringLiteral("Unknown variant type to write: %1") + .arg(QString::fromLatin1(value.typeName())); + } +} + +EwsPropertyField::Type EwsPropertyField::type() const +{ + return d->mPropType; +} + +QString EwsPropertyField::uri() const +{ + if (d->mPropType == Field || d->mPropType == IndexedField) { + return d->mUri; + } else { + return QString(); + } +} diff --git a/resources/ews/ewsclient/ewsrecurrence.h b/resources/ews/ewsclient/ewsrecurrence.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsrecurrence.h @@ -0,0 +1,49 @@ +/* + 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. +*/ + +#ifndef EWSRECURRENCE_H +#define EWSRECURRENCE_H + +#include + +class QXmlStreamReader; + +class EwsRecurrence : public KCalCore::Recurrence +{ +public: + EwsRecurrence(); + explicit EwsRecurrence(QXmlStreamReader &reader); + EwsRecurrence(const EwsRecurrence &other); + EwsRecurrence &operator=(const EwsRecurrence &other); +private: + bool readRelativeYearlyRecurrence(QXmlStreamReader &reader); + bool readAbsoluteYearlyRecurrence(QXmlStreamReader &reader); + bool readRelativeMonthlyRecurrence(QXmlStreamReader &reader); + bool readAbsoluteMonthlyRecurrence(QXmlStreamReader &reader); + bool readWeeklyRecurrence(QXmlStreamReader &reader); + bool readDailyRecurrence(QXmlStreamReader &reader); + bool readEndDateRecurrence(QXmlStreamReader &reader); + bool readNumberedRecurrence(QXmlStreamReader &reader); + + bool readDow(QXmlStreamReader &reader, QBitArray &dow); +}; + +Q_DECLARE_METATYPE(EwsRecurrence) + +#endif diff --git a/resources/ews/ewsclient/ewsrecurrence.cpp b/resources/ews/ewsclient/ewsrecurrence.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsrecurrence.cpp @@ -0,0 +1,564 @@ +/* + 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 KCalCore; + +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()) { + if (reader.namespaceUri() != ewsTypeNsUri) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected namespace in %1 element:").arg(reader.name().toString()) + << reader.namespaceUri(); + return; + } + + if (reader.name() == QStringLiteral("RelativeYearlyRecurrence")) { + if (!readRelativeYearlyRecurrence(reader)) { + return; + } + } else if (reader.name() == QStringLiteral("AbsoluteYearlyRecurrence")) { + if (!readAbsoluteYearlyRecurrence(reader)) { + return; + } + } else if (reader.name() == QStringLiteral("RelativeMonthlyRecurrence")) { + if (!readRelativeMonthlyRecurrence(reader)) { + return; + } + } else if (reader.name() == QStringLiteral("AbsoluteMonthlyRecurrence")) { + if (!readAbsoluteMonthlyRecurrence(reader)) { + return; + } + } else if (reader.name() == QStringLiteral("WeeklyRecurrence")) { + if (!readWeeklyRecurrence(reader)) { + return; + } + } else if (reader.name() == QStringLiteral("DailyRecurrence")) { + if (!readWeeklyRecurrence(reader)) { + return; + } + } else if (reader.name() == QStringLiteral("NoEndRecurrence")) { + // Ignore - this is the default + reader.skipCurrentElement(); + } else if (reader.name() == QStringLiteral("EndDateRecurrence")) { + if (!readEndDateRecurrence(reader)) { + return; + } + } else if (reader.name() == QStringLiteral("NumberedRecurrence")) { + if (!readNumberedRecurrence(reader)) { + return; + } + } else { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - unknown element: %2.") + .arg(QStringLiteral("Recurrence")).arg(reader.name().toString()); + return; + } + } +} + +EwsRecurrence::EwsRecurrence(const EwsRecurrence &other) + : Recurrence(other) +{ + +} + +EwsRecurrence &EwsRecurrence::operator=(const EwsRecurrence &other) +{ + *static_cast(this) = static_cast(other); + + return *this; +} + +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()) { + if (reader.namespaceUri() != ewsTypeNsUri) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected namespace in %1 element:").arg(reader.name().toString()) + << reader.namespaceUri(); + return false; + } + + if (reader.name() == QStringLiteral("DaysOfWeek")) { + if (!readDow(reader, dow)) { + return false; + } + hasDow = true; + } else if (reader.name() == QStringLiteral("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 (reader.name() == QStringLiteral("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 %1 element - unknown element: %2.") + .arg(QStringLiteral("RelativeYearlyRecurrence")).arg(reader.name().toString()); + 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()) { + if (reader.namespaceUri() != ewsTypeNsUri) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected namespace in %1 element:").arg(reader.name().toString()) + << reader.namespaceUri(); + return false; + } + + if (reader.name() == QStringLiteral("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 (reader.name() == QStringLiteral("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(reader.name().toString()); + 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()) { + if (reader.namespaceUri() != ewsTypeNsUri) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected namespace in %1 element:").arg(reader.name().toString()) + << reader.namespaceUri(); + return false; + } + + if (reader.name() == QStringLiteral("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 (reader.name() == QStringLiteral("DaysOfWeek")) { + if (!readDow(reader, dow)) { + return false; + } + hasDow = true; + } else if (reader.name() == QStringLiteral("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(reader.name().toString()); + 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()) { + if (reader.namespaceUri() != ewsTypeNsUri) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected namespace in %1 element:").arg(reader.name().toString()) + << reader.namespaceUri(); + return false; + } + + if (reader.name() == QStringLiteral("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 (reader.name() == QStringLiteral("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(reader.name().toString()); + 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()) { + if (reader.namespaceUri() != ewsTypeNsUri) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected namespace in %1 element:").arg(reader.name().toString()) + << reader.namespaceUri(); + return false; + } + + if (reader.name() == QStringLiteral("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 (reader.name() == QStringLiteral("DaysOfWeek")) { + if (!readDow(reader, dow)) { + return false; + } + hasDow = true; + } else if (reader.name() == QStringLiteral("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(reader.name().toString()); + 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()) { + if (reader.namespaceUri() != ewsTypeNsUri) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected namespace in %1 element:").arg(reader.name().toString()) + << reader.namespaceUri(); + return false; + } + + if (reader.name() == QStringLiteral("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; + } + } else { + qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read recurrence element - unknown element: %1.") + .arg(reader.name().toString()); + 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()) { + if (reader.namespaceUri() != ewsTypeNsUri) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected namespace in %1 element:").arg(reader.name().toString()) + << reader.namespaceUri(); + return false; + } + + if (reader.name() == QStringLiteral("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 (reader.name() == QStringLiteral("StartDate")) { + // Don't care + reader.skipCurrentElement(); + } else { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - unknown element: %2.") + .arg(QStringLiteral("EndDateRecurrence")).arg(reader.name().toString()); + return false; + } + } + + setEndDate(dateEnd); + + return true; +} + +bool EwsRecurrence::readNumberedRecurrence(QXmlStreamReader &reader) +{ + int numOccurrences = 0; + + 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() == QStringLiteral("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 (reader.name() == QStringLiteral("StartDate")) { + // Don't care + reader.skipCurrentElement(); + } else { + qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read recurrence element - unknown element: %1.") + .arg(reader.name().toString()); + return false; + } + } + + setDuration(numOccurrences); + + return true; +} + +bool EwsRecurrence::readDow(QXmlStreamReader &reader, QBitArray &dow) +{ + bool ok; + QString text = reader.readElementText(); + QStringList days = text.split(QChar::fromLatin1(' ')); + Q_FOREACH (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 == 8) { // "WeekendDay" + dow.fill(true, 5, 7); + } else { + dow.setBit(dowIndex); + } + } + return true; +} + diff --git a/resources/ews/ewsclient/ewsrequest.h b/resources/ews/ewsclient/ewsrequest.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsrequest.h @@ -0,0 +1,112 @@ +/* + 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 EWSREQUEST_H +#define EWSREQUEST_H + +#include + +#include +#include +#include +#include + +#include + +#include "ewsclient.h" +#include "ewsjob.h" +#include "ewsserverversion.h" +#include "ewstypes.h" + +class EwsRequest : public EwsJob +{ + Q_OBJECT +public: + class Response + { + public: + EwsResponseClass responseClass() const + { + return mClass; + }; + bool isSuccess() const + { + return mClass == EwsResponseSuccess; + }; + QString responseCode() const + { + return mCode; + }; + QString responseMessage() const + { + return mMessage; + }; + protected: + Response(QXmlStreamReader &reader); + bool readResponseElement(QXmlStreamReader &reader); + bool setErrorMsg(const QString &msg); + + EwsResponseClass mClass; + QString mCode; + QString mMessage; + }; + + EwsRequest(EwsClient &client, QObject *parent); + virtual ~EwsRequest(); + + void setMetaData(const KIO::MetaData &md); + void addMetaData(const QString &key, const QString &value); + + void setServerVersion(const EwsServerVersion &version); + const EwsServerVersion &serverVersion() const + { + return mServerVersion; + }; + + void dump() const; + +protected: + typedef std::function ContentReaderFn; + + void doSend(); + void prepare(const QString &body); + virtual bool parseResult(QXmlStreamReader &reader) = 0; + void startSoapDocument(QXmlStreamWriter &writer); + void endSoapDocument(QXmlStreamWriter &writer); + bool parseResponseMessage(QXmlStreamReader &reader, const QString &reqName, + ContentReaderFn contentReader); + bool readResponse(QXmlStreamReader &reader); + + KIO::MetaData mMd; + QString mResponseData; +protected Q_SLOTS: + void requestResult(KJob *job); + void requestData(KIO::Job *job, const QByteArray &data); +private: + bool readSoapBody(QXmlStreamReader &reader); + bool readSoapFault(QXmlStreamReader &reader); + bool readHeader(QXmlStreamReader &reader); + bool readResponseAttr(const QXmlStreamAttributes &attrs, EwsResponseClass &responseClass); + + QString mBody; + EwsClient &mClient; + EwsServerVersion mServerVersion; +}; + +#endif diff --git a/resources/ews/ewsclient/ewsrequest.cpp b/resources/ews/ewsclient/ewsrequest.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsrequest.cpp @@ -0,0 +1,348 @@ +/* + 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 "ewsrequest.h" + +#include + +#include "ewsclient.h" +#include "ewsclient_debug.h" +#include "ewsserverversion.h" + +EwsRequest::EwsRequest(EwsClient &client, QObject *parent) + : EwsJob(parent), mClient(client), mServerVersion(EwsServerVersion::ewsVersion2007Sp1) +{ +} + +EwsRequest::~EwsRequest() +{ +} + +void EwsRequest::doSend() +{ + Q_FOREACH (KJob *job, subjobs()) { + job->start(); + } +} + +void EwsRequest::startSoapDocument(QXmlStreamWriter &writer) +{ + writer.setCodec("UTF-8"); + + writer.writeStartDocument(); + + writer.writeNamespace(soapEnvNsUri, QStringLiteral("soap")); + writer.writeNamespace(ewsMsgNsUri, QStringLiteral("m")); + writer.writeNamespace(ewsTypeNsUri, QStringLiteral("t")); + + // SOAP Envelope + writer.writeStartElement(soapEnvNsUri, QStringLiteral("Envelope")); + + // SOAP Header + writer.writeStartElement(soapEnvNsUri, QStringLiteral("Header")); + mServerVersion.writeRequestServerVersion(writer); + writer.writeEndElement(); + + // SOAP Body + writer.writeStartElement(soapEnvNsUri, QStringLiteral("Body")); +} + +void EwsRequest::endSoapDocument(QXmlStreamWriter &writer) +{ + // End SOAP Body + writer.writeEndElement(); + + // End SOAP Envelope + writer.writeEndElement(); + + writer.writeEndDocument(); +} + +void EwsRequest::prepare(const QString &body) +{ + mBody = body; + KIO::TransferJob *job = KIO::http_post(mClient.url(), body.toUtf8(), + KIO::HideProgressInfo); + job->addMetaData(QStringLiteral("content-type"), QStringLiteral("text/xml")); + job->addMetaData(QStringLiteral("no-auth-prompt"), QStringLiteral("true")); + if (mClient.isNTLMv2Enabled()) { + job->addMetaData(QStringLiteral("EnableNTLMv2Auth"), QStringLiteral("true")); + } + if (!mClient.userAgent().isEmpty()) { + job->addMetaData(QStringLiteral("UserAgent"), mClient.userAgent()); + } + job->addMetaData(mMd); + + connect(job, &KIO::TransferJob::result, this, &EwsRequest::requestResult); + connect(job, &KIO::TransferJob::data, this, &EwsRequest::requestData); + + addSubjob(job); +} + +void EwsRequest::setMetaData(const KIO::MetaData &md) +{ + mMd = md; +} + +void EwsRequest::addMetaData(const QString &key, const QString &value) +{ + mMd.insert(key, value); +} + +void EwsRequest::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(), job->error()); + } + /* Don't attempt to parse the response in case of a HTTP error. The only exception is + * 500 (Bad Request) as in such case the server does provide the usual SOAP response. */ + else if ((resp >= 300) && (resp != 500)) { + setErrorMsg(QStringLiteral("Failed to process EWS request - HTTP code %1").arg(resp)); + setError(resp); + } else { + QXmlStreamReader reader(mResponseData); + readResponse(reader); + } + + emitResult(); +} + +bool EwsRequest::readResponse(QXmlStreamReader &reader) +{ + if (!reader.readNextStartElement()) { + return setErrorMsg(QStringLiteral("Failed to read EWS request XML")); + } + + if ((reader.name() != QStringLiteral("Envelope")) || (reader.namespaceUri() != soapEnvNsUri)) { + return setErrorMsg(QStringLiteral("Failed to read EWS request - not a SOAP XML")); + } + + while (reader.readNextStartElement()) { + if (reader.namespaceUri() != soapEnvNsUri) { + return setErrorMsg(QStringLiteral("Failed to read EWS request - not a SOAP XML")); + } + + if (reader.name() == QStringLiteral("Body")) { + if (!readSoapBody(reader)) + return false; + } else if (reader.name() == QStringLiteral("Header")) { + if (!readHeader(reader)) { + return false; + } + } + } + return true; +} + +bool EwsRequest::readSoapBody(QXmlStreamReader &reader) +{ + while (reader.readNextStartElement()) { + if ((reader.name() == QStringLiteral("Fault")) + && (reader.namespaceUri() == soapEnvNsUri)) { + return readSoapFault(reader); + } + + if (!parseResult(reader)) { + if (EWSCLI_FAILEDREQUEST_LOG().isDebugEnabled()) { + dump(); + } + return false; + } + } + return true; +} + +bool EwsRequest::readSoapFault(QXmlStreamReader &reader) +{ + QString faultCode; + QString faultString; + while (reader.readNextStartElement()) { + if (reader.name() == QStringLiteral("faultcode")) { + faultCode = reader.readElementText(); + } else if (reader.name() == QStringLiteral("faultstring")) { + faultString = reader.readElementText(); + } + } + + setErrorMsg(faultCode + QStringLiteral(": ") + faultString); + + if (EWSCLI_FAILEDREQUEST_LOG().isDebugEnabled()) { + dump(); + } + + return false; +} + +void EwsRequest::requestData(KIO::Job *job, const QByteArray &data) +{ + Q_UNUSED(job); + + qCDebug(EWSCLI_PROTO_LOG) << "data" << job << data; + mResponseData += QString::fromUtf8(data); +} + +bool EwsRequest::parseResponseMessage(QXmlStreamReader &reader, const QString &reqName, + ContentReaderFn contentReader) +{ + if (reader.name() != reqName + QStringLiteral("Response") + || reader.namespaceUri() != ewsMsgNsUri) { + return setErrorMsg(QStringLiteral("Failed to read EWS request - expected %1 element.") + .arg(reqName + QStringLiteral("Response"))); + } + + if (!reader.readNextStartElement()) { + return setErrorMsg(QStringLiteral("Failed to read EWS request - expected a child element in %1 element.") + .arg(reqName + QStringLiteral("Response"))); + } + + if (reader.name() != QStringLiteral("ResponseMessages") + || reader.namespaceUri() != ewsMsgNsUri) { + return setErrorMsg(QStringLiteral("Failed to read EWS request - expected %1 element.") + .arg(QStringLiteral("ResponseMessages"))); + } + + while (reader.readNextStartElement()) { + if (reader.name() != reqName + QStringLiteral("ResponseMessage") + || reader.namespaceUri() != ewsMsgNsUri) { + return setErrorMsg(QStringLiteral("Failed to read EWS request - expected %1 element.") + .arg(reqName + QStringLiteral("ResponseMessage"))); + } + + if (!contentReader(reader)) { + return false; + } + } + + return true; +} + +void EwsRequest::setServerVersion(const EwsServerVersion &version) +{ + mServerVersion = version; +} + +EwsRequest::Response::Response(QXmlStreamReader &reader) +{ + static const QString respClasses[] = { + QStringLiteral("Success"), + QStringLiteral("Warning"), + QStringLiteral("Error") + }; + + QStringRef respClassRef = reader.attributes().value(QStringLiteral("ResponseClass")); + if (respClassRef.isNull()) { + mClass = EwsResponseParseError; + qCWarning(EWSCLI_LOG) << "ResponseClass attribute not found in response element"; + return; + } + + unsigned i; + for (i = 0; i < sizeof(respClasses) / sizeof(respClasses[0]); ++i) { + if (respClassRef == respClasses[i]) { + mClass = static_cast(i); + break; + } + } +} + +bool EwsRequest::Response::readResponseElement(QXmlStreamReader &reader) +{ + if (reader.namespaceUri() != ewsMsgNsUri) { + return false; + } + if (reader.name() == QStringLiteral("ResponseCode")) { + mCode = reader.readElementText(); + } else if (reader.name() == QStringLiteral("MessageText")) { + mMessage = reader.readElementText(); + } else if (reader.name() == QStringLiteral("DescriptiveLinkKey")) { + reader.skipCurrentElement(); + } else if (reader.name() == QStringLiteral("MessageXml")) { + reader.skipCurrentElement(); + } else if (reader.name() == QStringLiteral("ErrorSubscriptionIds")) { + reader.skipCurrentElement(); + } else { + return false; + } + return true; +} + +bool EwsRequest::readHeader(QXmlStreamReader &reader) +{ + while (reader.readNextStartElement()) { + if (reader.name() == QStringLiteral("ServerVersionInfo") && reader.namespaceUri() == ewsTypeNsUri) { + EwsServerVersion version(reader); + if (!version.isValid()) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read EWS request - error parsing server version."); + return false; + } + mServerVersion = version; + mClient.setServerVersion(version); + reader.skipCurrentElement(); + } else { + reader.skipCurrentElement(); + } + } + + return true; +} + +bool EwsRequest::Response::setErrorMsg(const QString &msg) +{ + mClass = EwsResponseParseError; + mCode = QStringLiteral("ResponseParseError"); + mMessage = msg; + qCWarningNC(EWSCLI_LOG) << msg; + return false; +} + +void EwsRequest::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/ewsserverversion.h b/resources/ews/ewsclient/ewsserverversion.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsserverversion.h @@ -0,0 +1,129 @@ +/* + 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 EWSSERVERVERSION_H +#define EWSSERVERVERSION_H + +#include + +class QXmlStreamReader; +class QXmlStreamWriter; + +class EwsServerVersion +{ +public: + enum ServerFeature { + StreamingSubscription, + FreeBusyChangedEvent, + }; + + EwsServerVersion() : mMajor(0), mMinor(0), mMajorBuild(0), mMinorBuild(0) {}; + EwsServerVersion(uint major, uint minor, const QString &name, const QString &friendlyName) + : mMajor(major), mMinor(minor), mMajorBuild(0), mMinorBuild(0), mName(name), + mFriendlyName(friendlyName) {}; + explicit EwsServerVersion(QXmlStreamReader &reader); + EwsServerVersion(const EwsServerVersion &other) + : mMajor(other.mMajor), mMinor(other.mMinor), mMajorBuild(other.mMajorBuild), + mMinorBuild(other.mMinorBuild), mName(other.mName), mFriendlyName(other.mFriendlyName) {}; + + EwsServerVersion &operator=(const EwsServerVersion &other) + { + mMajor = other.mMajor; + mMinor = other.mMinor; + mMajorBuild = other.mMajorBuild; + mMinorBuild = other.mMinorBuild; + mName = other.mName; + mFriendlyName = other.mFriendlyName; + return *this; + } + + void writeRequestServerVersion(QXmlStreamWriter &writer) const; + + bool operator>(const EwsServerVersion &other) const + { + return (mMajor > other.mMajor) ? true : ((mMinor > other.mMinor) ? true : false); + } + bool operator<(const EwsServerVersion &other) const + { + return (mMajor < other.mMajor) ? true : ((mMinor < other.mMinor) ? true : false); + } + bool operator>=(const EwsServerVersion &other) const + { + return !(*this < other); + } + bool operator<=(const EwsServerVersion &other) const + { + return !(*this > other); + } + bool operator==(const EwsServerVersion &other) const + { + return (mMajor == other.mMajor) && (mMinor == other.mMinor); + } + bool operator!=(const EwsServerVersion &other) const + { + return !(*this == other); + } + + bool supports(ServerFeature feature) const; + + bool isValid() const + { + return mMajor != 0; + }; + + uint majorVersion() const + { + return mMajor; + }; + uint minorVersion() const + { + return mMinor; + }; + QString name() const + { + return mName; + }; + + static const EwsServerVersion &minSupporting(ServerFeature feature); + + QString toString() const; + + static const EwsServerVersion ewsVersion2007; + static const EwsServerVersion ewsVersion2007Sp1; + static const EwsServerVersion ewsVersion2007Sp2; + static const EwsServerVersion ewsVersion2007Sp3; + static const EwsServerVersion ewsVersion2010; + static const EwsServerVersion ewsVersion2010Sp1; + static const EwsServerVersion ewsVersion2010Sp2; + static const EwsServerVersion ewsVersion2010Sp3; + static const EwsServerVersion ewsVersion2013; + static const EwsServerVersion ewsVersion2016; + +private: + uint mMajor; + uint mMinor; + uint mMajorBuild; + uint mMinorBuild; + QString mName; + QString mFriendlyName; +}; + +QDebug operator<<(const QDebug dbg, const EwsServerVersion &version); + +#endif diff --git a/resources/ews/ewsclient/ewsserverversion.cpp b/resources/ews/ewsclient/ewsserverversion.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsserverversion.cpp @@ -0,0 +1,157 @@ +/* + 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 "ewsserverversion.h" + +#include +#include + +#include "ewsclient_debug.h" +#include "ewstypes.h" + +const EwsServerVersion EwsServerVersion::ewsVersion2007(8, 0, QStringLiteral("Exchange2007"), + QStringLiteral("Exchange 2007")); +const EwsServerVersion EwsServerVersion::ewsVersion2007Sp1(8, 1, QStringLiteral("Exchange2007_SP1"), + QStringLiteral("Exchange 2007 SP1")); +const EwsServerVersion EwsServerVersion::ewsVersion2007Sp2(8, 2, QStringLiteral("Exchange2007_SP2"), + QStringLiteral("Exchange 2007 SP2")); +const EwsServerVersion EwsServerVersion::ewsVersion2007Sp3(8, 3, QStringLiteral("Exchange2007_SP3"), + QStringLiteral("Exchange 2007 SP3")); +const EwsServerVersion EwsServerVersion::ewsVersion2010(14, 0, QStringLiteral("Exchange2010"), + QStringLiteral("Exchange 2010")); +const EwsServerVersion EwsServerVersion::ewsVersion2010Sp1(14, 1, QStringLiteral("Exchange2010_SP1"), + QStringLiteral("Exchange 2010 SP1")); +const EwsServerVersion EwsServerVersion::ewsVersion2010Sp2(14, 2, QStringLiteral("Exchange2010_SP2"), + QStringLiteral("Exchange 2010 SP2")); +const EwsServerVersion EwsServerVersion::ewsVersion2010Sp3(14, 3, QStringLiteral("Exchange2010_SP3"), + QStringLiteral("Exchange 2010 SP3")); +const EwsServerVersion EwsServerVersion::ewsVersion2013(15, 0, QStringLiteral("Exchange2013"), + QStringLiteral("Exchange 2013")); +const EwsServerVersion EwsServerVersion::ewsVersion2016(15, 1, QStringLiteral("Exchange2016"), + QStringLiteral("Exchange 2016")); + +static const EwsServerVersion ewsNullVersion; + +EwsServerVersion::EwsServerVersion(QXmlStreamReader &reader) + : mMajor(0), mMinor(0) +{ + // + QXmlStreamAttributes attrs = reader.attributes(); + + QStringRef majorRef = attrs.value(QStringLiteral("MajorVersion")); + QStringRef minorRef = attrs.value(QStringLiteral("MinorVersion")); + QStringRef majorBuildRef = attrs.value(QStringLiteral("MajorBuildNumber")); + QStringRef minorBuildRef = attrs.value(QStringLiteral("MinorBuildNumber")); + QStringRef nameRef = attrs.value(QStringLiteral("Version")); + + if (majorRef.isNull() || minorRef.isNull()) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading server version info - missing attribute."); + return; + } + + bool ok; + uint majorVer = majorRef.toUInt(&ok); + if (!ok) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading server version info - invalid major version number."); + return; + } + uint minorVer = minorRef.toUInt(&ok); + if (!ok) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading server version info - invalid minor version number."); + return; + } + + mMajor = majorVer; + mMinor = minorVer; + mMajorBuild = majorBuildRef.toUInt(); + mMinorBuild = minorBuildRef.toUInt(); + mName = nameRef.toString(); +} + +void EwsServerVersion::writeRequestServerVersion(QXmlStreamWriter &writer) const +{ + writer.writeStartElement(ewsTypeNsUri, QStringLiteral("RequestServerVersion")); + writer.writeAttribute(QStringLiteral("Version"), mName); + writer.writeEndElement(); +} + +bool EwsServerVersion::supports(ServerFeature feature) const +{ + const EwsServerVersion &minVer = minSupporting(feature); + if (minVer.isValid()) { + return *this >= minVer; + } else { + return false; + } +} + +const EwsServerVersion &EwsServerVersion::minSupporting(ServerFeature feature) +{ + switch (feature) { + case StreamingSubscription: + case FreeBusyChangedEvent: + return ewsVersion2010Sp1; + default: + return ewsNullVersion; + } +} + +QString EwsServerVersion::toString() const +{ + static const QVector knownVersions = { + ewsVersion2007, + ewsVersion2007Sp1, + ewsVersion2007Sp2, + ewsVersion2007Sp3, + ewsVersion2010, + ewsVersion2010Sp1, + ewsVersion2010Sp2, + ewsVersion2010Sp3, + ewsVersion2013, + ewsVersion2016 + }; + + QString version(QStringLiteral("%1.%2").arg(mMajor).arg(mMinor)); + + if (mMajorBuild + mMinorBuild > 0) { + version.append(QStringLiteral(".%1.%2").arg(mMajorBuild).arg(mMinorBuild)); + } + + Q_FOREACH (const EwsServerVersion &ver, knownVersions) { + if (*this == ver) { + version.append(QStringLiteral(" (") + ver.mFriendlyName + QStringLiteral(")")); + } + } + + return version; +} + +QDebug operator<<(QDebug debug, const EwsServerVersion &version) +{ + QDebugStateSaver saver(debug); + QDebug d = debug.nospace().noquote(); + d << QStringLiteral("EwsServerVersion("); + if (version.isValid()) { + d << version.majorVersion() << QStringLiteral(", ") << version.minorVersion() + << QStringLiteral(", ") << version.name(); + } + d << QStringLiteral(")"); + return debug; +} diff --git a/resources/ews/ewsclient/ewssubscriberequest.h b/resources/ews/ewsclient/ewssubscriberequest.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewssubscriberequest.h @@ -0,0 +1,111 @@ +/* + 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 EWSSUBSCRIBEREQUEST_H +#define EWSSUBSCRIBEREQUEST_H + +#include + +#include "ewsid.h" +#include "ewsrequest.h" +#include "ewstypes.h" + +class QXmlStreamWriter; + +class EwsSubscribeRequest : public EwsRequest +{ + Q_OBJECT +public: + + enum Type { + PullSubscription = 0, + PushSubscription, + StreamingSubscription + }; + + class Response : public EwsRequest::Response + { + public: + const QString &subscriptionId() const + { + return mId; + }; + const QString &watermark() const + { + return mWatermark; + }; + protected: + Response(QXmlStreamReader &reader); + + QString mId; + QString mWatermark; + + friend class EwsSubscribeRequest; + }; + + EwsSubscribeRequest(EwsClient &client, QObject *parent); + virtual ~EwsSubscribeRequest(); + + void setType(Type t) + { + mType = t; + }; + void setFolderIds(EwsId::List folders) + { + mFolderIds = folders; + }; + void setAllFolders(bool allFolders) + { + mAllFolders = allFolders; + }; + void setEventTypes(QList types) + { + mEventTypes = types; + }; + void setWatermark(const QString &watermark) + { + mWatermark = watermark; + }; + void setTimeout(uint timeout) + { + mTimeout = timeout; + }; + + const Response &response() const + { + return *mResponse; + }; + + virtual void start() override; +protected: + virtual bool parseResult(QXmlStreamReader &reader) override; + bool parseSubscribeResponse(QXmlStreamReader &reader); +private: + //QSharedPointer mSubscription; + Type mType; + EwsId::List mFolderIds; + QList mEventTypes; + bool mAllFolders; + QString mWatermark; + uint mTimeout; + + QSharedPointer mResponse; +}; + +#endif diff --git a/resources/ews/ewsclient/ewssubscriberequest.cpp b/resources/ews/ewsclient/ewssubscriberequest.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewssubscriberequest.cpp @@ -0,0 +1,159 @@ +/* + 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 "ewssubscriberequest.h" + +#include + +#include "ewsclient_debug.h" + +static const QVector subscribeTypeNames = { + QStringLiteral("PullSubscriptionRequest"), + QStringLiteral("PushSubscriptionRequest"), + QStringLiteral("StreamingSubscriptionRequest") +}; + +static const QVector eventTypeNames = { + QStringLiteral("CopiedEvent"), + QStringLiteral("CreatedEvent"), + QStringLiteral("DeletedEvent"), + QStringLiteral("ModifiedEvent"), + QStringLiteral("MovedEvent"), + QStringLiteral("NewMailEvent"), + QStringLiteral("FreeBusyChangedEvent") +}; + +EwsSubscribeRequest::EwsSubscribeRequest(EwsClient &client, QObject *parent) + : EwsRequest(client, parent), mType(PullSubscription), mAllFolders(false), mTimeout(30) +{ +} + +EwsSubscribeRequest::~EwsSubscribeRequest() +{ +} + +void EwsSubscribeRequest::start() +{ + QString reqString; + QXmlStreamWriter writer(&reqString); + + if (mType == StreamingSubscription + && !serverVersion().supports(EwsServerVersion::StreamingSubscription)) { + setServerVersion(EwsServerVersion::minSupporting(EwsServerVersion::StreamingSubscription)); + } + if (mEventTypes.contains(EwsFreeBusyChangedEvent) + && !serverVersion().supports(EwsServerVersion::FreeBusyChangedEvent)) { + setServerVersion(EwsServerVersion::minSupporting(EwsServerVersion::FreeBusyChangedEvent)); + } + + startSoapDocument(writer); + + writer.writeStartElement(ewsMsgNsUri, QStringLiteral("Subscribe")); + + writer.writeStartElement(ewsMsgNsUri, subscribeTypeNames[mType]); + + if (mAllFolders) { + writer.writeAttribute(QStringLiteral("SubscribeToAllFolders"), QStringLiteral("true")); + } + + writer.writeStartElement(ewsTypeNsUri, QStringLiteral("FolderIds")); + Q_FOREACH (const EwsId &id, mFolderIds) { + id.writeFolderIds(writer); + } + writer.writeEndElement(); + + writer.writeStartElement(ewsTypeNsUri, QStringLiteral("EventTypes")); + Q_FOREACH (const EwsEventType type, mEventTypes) { + writer.writeTextElement(ewsTypeNsUri, QStringLiteral("EventType"), eventTypeNames[type]); + } + writer.writeEndElement(); // EventTypes + + if (mType == PullSubscription) { + if (!mWatermark.isNull()) { + writer.writeTextElement(ewsTypeNsUri, QStringLiteral("Watermark"), mWatermark); + } + writer.writeTextElement(ewsTypeNsUri, QStringLiteral("Timeout"), QString::number(mTimeout)); + } + + writer.writeEndElement(); // XxxSubscriptionRequest + + writer.writeEndElement(); // Subscribe + + endSoapDocument(writer); + + qCDebug(EWSCLI_PROTO_LOG) << reqString; + + prepare(reqString); + + doSend(); +} + +bool EwsSubscribeRequest::parseResult(QXmlStreamReader &reader) +{ + return parseResponseMessage(reader, QStringLiteral("Subscribe"), + [this](QXmlStreamReader &reader) {return parseSubscribeResponse(reader);}); +} + +bool EwsSubscribeRequest::parseSubscribeResponse(QXmlStreamReader &reader) +{ + QSharedPointer resp(new Response(reader)); + if (resp->responseClass() == EwsResponseUnknown) { + return false; + } + + mResponse = resp; + return true; +} + +EwsSubscribeRequest::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())); + return; + } + + if (reader.name() == QStringLiteral("SubscriptionId")) { + mId = reader.readElementText(); + + if (reader.error() != QXmlStreamReader::NoError) { + setErrorMsg(QStringLiteral("Failed to read EWS request - invalid %1 element.") + .arg(QStringLiteral("SubscriptionId"))); + return; + } + } else if (reader.name() == QStringLiteral("Watermark")) { + mWatermark = reader.readElementText(); + + if (reader.error() != QXmlStreamReader::NoError) { + setErrorMsg(QStringLiteral("Failed to read EWS request - invalid %1 element.") + .arg(QStringLiteral("Watermark"))); + return; + } + } else if (!readResponseElement(reader)) { + setErrorMsg(QStringLiteral("Failed to read EWS request - invalid response element.")); + return; + } + } +} diff --git a/resources/ews/ewsclient/ewssyncfolderhierarchyrequest.h b/resources/ews/ewsclient/ewssyncfolderhierarchyrequest.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewssyncfolderhierarchyrequest.h @@ -0,0 +1,108 @@ +/* + 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. +*/ + +#ifndef EWSSYNCFOLDERHIERARCHYREQUEST_H +#define EWSSYNCFOLDERHIERARCHYREQUEST_H + +#include "ewsfolder.h" +#include "ewsrequest.h" +#include "ewstypes.h" +#include "ewsfoldershape.h" + +class EwsSyncFolderHierarchyRequest : public EwsRequest +{ + Q_OBJECT +public: + enum ChangeType { + Create, + Update, + Delete, + Unknown + }; + + class Response; + + class Change + { + public: + typedef QList List; + + ChangeType type() const + { + return mType; + }; + const EwsId &folderId() const + { + return mId; + }; + const EwsFolder &folder() const + { + return mFolder; + }; + protected: + Change(QXmlStreamReader &reader); + bool isValid() const + { + return mType != Unknown; + }; + + ChangeType mType; + EwsId mId; + EwsFolder mFolder; + + friend class Response; + }; + + EwsSyncFolderHierarchyRequest(EwsClient &client, QObject *parent); + virtual ~EwsSyncFolderHierarchyRequest(); + + void setFolderId(const EwsId &id); + void setFolderShape(const EwsFolderShape &shape); + void setSyncState(const QString &state); + + virtual void start() override; + + bool includesLastItem() const + { + return mIncludesLastItem; + }; + + const Change::List &changes() const + { + return mChanges; + }; + const QString &syncState() const + { + return mSyncState; + }; +protected: + virtual bool parseResult(QXmlStreamReader &reader) override; + bool parseItemsResponse(QXmlStreamReader &reader); +private: + + EwsId mFolderId; + EwsFolderShape mShape; + QString mSyncState; + Change::List mChanges; + bool mIncludesLastItem; +}; + +Q_DECLARE_METATYPE(EwsSyncFolderHierarchyRequest::Change::List) + +#endif diff --git a/resources/ews/ewsclient/ewssyncfolderhierarchyrequest.cpp b/resources/ews/ewsclient/ewssyncfolderhierarchyrequest.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewssyncfolderhierarchyrequest.cpp @@ -0,0 +1,240 @@ +/* + 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 "ewssyncfolderhierarchyrequest.h" + +#include + +#include + +#include "ewsclient_debug.h" +#include "ewsxml.h" + +enum SyncFolderHierarchyResponseElementType { + SyncState, + IncludesLastFolderInRange, + Changes +}; + +enum SyncFolderHierarchyChangeElementType { + Folder, + FolderId, + IsRead +}; + +class EwsSyncFolderHierarchyRequest::Response : public EwsRequest::Response +{ +public: + Response(QXmlStreamReader &reader); + + static bool changeReader(QXmlStreamReader &reader, QVariant &val); + + EwsSyncFolderHierarchyRequest::Change::List mChanges; + bool mIncludesLastFolder; + QString mSyncState; +}; + +EwsSyncFolderHierarchyRequest::EwsSyncFolderHierarchyRequest(EwsClient &client, QObject *parent) + : EwsRequest(client, parent), mIncludesLastItem(false) +{ + qRegisterMetaType(); + qRegisterMetaType(); +} + +EwsSyncFolderHierarchyRequest::~EwsSyncFolderHierarchyRequest() +{ +} + +void EwsSyncFolderHierarchyRequest::setFolderId(const EwsId &id) +{ + mFolderId = id; +} + +void EwsSyncFolderHierarchyRequest::setFolderShape(const EwsFolderShape &shape) +{ + mShape = shape; +} + +void EwsSyncFolderHierarchyRequest::setSyncState(const QString &state) +{ + mSyncState = state; +} + +void EwsSyncFolderHierarchyRequest::start() +{ + QString reqString; + QXmlStreamWriter writer(&reqString); + + startSoapDocument(writer); + + writer.writeStartElement(ewsMsgNsUri, QStringLiteral("SyncFolderHierarchy")); + + mShape.write(writer); + + writer.writeStartElement(ewsMsgNsUri, QStringLiteral("SyncFolderId")); + mFolderId.writeFolderIds(writer); + writer.writeEndElement(); + + if (!mSyncState.isNull()) { + writer.writeTextElement(ewsMsgNsUri, QStringLiteral("SyncState"), mSyncState); + } + + writer.writeEndElement(); + + endSoapDocument(writer); + + qCDebug(EWSCLI_PROTO_LOG) << reqString; + + if (EWSCLI_REQUEST_LOG().isDebugEnabled()) { + QString st = mSyncState.isNull() ? QStringLiteral("none") : QString::number(qHash(mSyncState), 36); + QString folder; + qCDebugNCS(EWSCLI_REQUEST_LOG) << QStringLiteral("Starting SyncFolderHierarchy request (folder: ") + << mFolderId << QStringLiteral(", state: %1").arg(st); + } + + prepare(reqString); + + doSend(); +} + +bool EwsSyncFolderHierarchyRequest::parseResult(QXmlStreamReader &reader) +{ + return parseResponseMessage(reader, QStringLiteral("SyncFolderHierarchy"), + [this](QXmlStreamReader &reader) {return parseItemsResponse(reader);}); +} + +bool EwsSyncFolderHierarchyRequest::parseItemsResponse(QXmlStreamReader &reader) +{ + EwsSyncFolderHierarchyRequest::Response *resp = new EwsSyncFolderHierarchyRequest::Response(reader); + if (resp->responseClass() == EwsResponseUnknown) { + return false; + } + + mChanges = resp->mChanges; + mSyncState = resp->mSyncState; + mIncludesLastItem = resp->mIncludesLastFolder; + + if (EWSCLI_REQUEST_LOG().isDebugEnabled()) { + if (resp->isSuccess()) { + qCDebugNC(EWSCLI_REQUEST_LOG) << QStringLiteral("Got SyncFolderHierarchy response (%1 changes, state: %3)") + .arg(mChanges.size()).arg(qHash(mSyncState), 0, 36); + } else { + qCDebugNC(EWSCLI_REQUEST_LOG) << QStringLiteral("Got SyncFolderHierarchy response - %1") + .arg(resp->responseMessage()); + } + } + + return true; +} + +EwsSyncFolderHierarchyRequest::Response::Response(QXmlStreamReader &reader) + : EwsRequest::Response(reader) +{ + if (mClass == EwsResponseParseError) { + return; + } + + static const QVector::Item> items = { + {SyncState, QStringLiteral("SyncState"), &ewsXmlTextReader}, + {IncludesLastFolderInRange, QStringLiteral("IncludesLastFolderInRange"), &ewsXmlBoolReader}, + {Changes, QStringLiteral("Changes"), &EwsSyncFolderHierarchyRequest::Response::changeReader}, + }; + static const EwsXml staticReader(items); + + EwsXml ewsReader(staticReader); + + if (!ewsReader.readItems(reader, ewsMsgNsUri, + [this](QXmlStreamReader &reader, const QString &) { + if (!readResponseElement(reader)) { + setErrorMsg(QStringLiteral("Failed to read EWS request - invalid response element.")); + return false; + } + return true; + })) + { + mClass = EwsResponseParseError; + return; + } + + QHash values = ewsReader.values(); + + mSyncState = values[SyncState].toString(); + mIncludesLastFolder = values[IncludesLastFolderInRange].toBool(); + mChanges = values[Changes].value(); +} + +bool EwsSyncFolderHierarchyRequest::Response::changeReader(QXmlStreamReader &reader, QVariant &val) +{ + Change::List changes; + QString elmName(reader.name().toString()); + + while (reader.readNextStartElement()) { + Change change(reader); + if (!change.isValid()) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element").arg(elmName); + return false; + } + changes.append(change); + } + + val = QVariant::fromValue(changes); + return true; +} + +EwsSyncFolderHierarchyRequest::Change::Change(QXmlStreamReader &reader) +{ + static const QVector::Item> items = { + {Folder, QStringLiteral("Folder"), &ewsXmlFolderReader}, + {Folder, QStringLiteral("CalendarFolder"), &ewsXmlFolderReader}, + {Folder, QStringLiteral("ContactsFolder"), &ewsXmlFolderReader}, + {Folder, QStringLiteral("SearchFolder"), &ewsXmlFolderReader}, + {Folder, QStringLiteral("TasksFolder"), &ewsXmlFolderReader}, + {FolderId, QStringLiteral("FolderId"), &ewsXmlIdReader}, + {IsRead, QStringLiteral("IsRead"), &ewsXmlBoolReader} + }; + static const EwsXml staticReader(items); + + EwsXml ewsReader(staticReader); + + if (reader.name() == QStringLiteral("Create")) { + mType = Create; + } else if (reader.name() == QStringLiteral("Update")) { + mType = Update; + } else if (reader.name() == QStringLiteral("Delete")) { + mType = Delete; + } + if (!ewsReader.readItems(reader, ewsTypeNsUri)) { + return; + } + + QHash values = ewsReader.values(); + + switch (mType) { + case Create: + case Update: + mFolder = values[Folder].value(); + mId = mFolder[EwsFolderFieldFolderId].value(); + break; + case Delete: + mId = values[FolderId].value(); + break; + default: + break; + } +} diff --git a/resources/ews/ewsclient/ewssyncfolderitemsrequest.h b/resources/ews/ewsclient/ewssyncfolderitemsrequest.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewssyncfolderitemsrequest.h @@ -0,0 +1,116 @@ +/* + 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. +*/ + +#ifndef EWSSYNCFOLDERITEMSREQUEST_H +#define EWSSYNCFOLDERITEMSREQUEST_H + +#include "ewsitem.h" +#include "ewsrequest.h" +#include "ewstypes.h" +#include "ewsitemshape.h" + +class EwsSyncFolderItemsRequest : public EwsRequest +{ + Q_OBJECT +public: + enum ChangeType { + Create, + Update, + Delete, + ReadFlagChange, + Unknown + }; + + class Response; + + class Change + { + public: + typedef QList List; + + ChangeType type() const + { + return mType; + }; + const EwsId &itemId() const + { + return mId; + }; + const EwsItem &item() const + { + return mItem; + }; + bool isRead() const + { + return mIsRead; + }; + protected: + Change(QXmlStreamReader &reader); + bool isValid() const + { + return mType != Unknown; + }; + + ChangeType mType; + EwsId mId; + EwsItem mItem; + bool mIsRead; + + friend class Response; + }; + + EwsSyncFolderItemsRequest(EwsClient &client, QObject *parent); + virtual ~EwsSyncFolderItemsRequest(); + + void setFolderId(const EwsId &id); + void setItemShape(const EwsItemShape &shape); + void setSyncState(const QString &state); + void setMaxChanges(uint max); + + virtual void start() override; + + bool includesLastItem() const + { + return mIncludesLastItem; + }; + + const Change::List &changes() const + { + return mChanges; + }; + const QString &syncState() const + { + return mSyncState; + }; +protected: + virtual bool parseResult(QXmlStreamReader &reader) override; + bool parseItemsResponse(QXmlStreamReader &reader); +private: + + EwsId mFolderId; + EwsItemShape mShape; + QString mSyncState; + uint mMaxChanges; + Change::List mChanges; + bool mIncludesLastItem; +}; + +Q_DECLARE_METATYPE(EwsSyncFolderItemsRequest::Change::List) + +#endif diff --git a/resources/ews/ewsclient/ewssyncfolderitemsrequest.cpp b/resources/ews/ewsclient/ewssyncfolderitemsrequest.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewssyncfolderitemsrequest.cpp @@ -0,0 +1,259 @@ +/* + 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 "ewssyncfolderitemsrequest.h" + +#include + +#include + +#include "ewsclient_debug.h" +#include "ewsxml.h" + +enum SyncFolderItemsResponseElementType { + SyncState, + IncludesLastItemInRange, + Changes +}; + +enum SyncFolderItemsChangeElementType { + Item, + ItemId, + IsRead +}; + +class EwsSyncFolderItemsRequest::Response : public EwsRequest::Response +{ +public: + Response(QXmlStreamReader &reader); + + static bool changeReader(QXmlStreamReader &reader, QVariant &val); + + EwsSyncFolderItemsRequest::Change::List mChanges; + bool mIncludesLastItem; + QString mSyncState; +}; + +EwsSyncFolderItemsRequest::EwsSyncFolderItemsRequest(EwsClient &client, QObject *parent) + : EwsRequest(client, parent), mMaxChanges(100), mIncludesLastItem(false) +{ + qRegisterMetaType(); + qRegisterMetaType(); +} + +EwsSyncFolderItemsRequest::~EwsSyncFolderItemsRequest() +{ +} + +void EwsSyncFolderItemsRequest::setFolderId(const EwsId &id) +{ + mFolderId = id; +} + +void EwsSyncFolderItemsRequest::setItemShape(const EwsItemShape &shape) +{ + mShape = shape; +} + +void EwsSyncFolderItemsRequest::setSyncState(const QString &state) +{ + mSyncState = state; +} + +void EwsSyncFolderItemsRequest::setMaxChanges(uint max) +{ + mMaxChanges = max; +} + +void EwsSyncFolderItemsRequest::start() +{ + QString reqString; + QXmlStreamWriter writer(&reqString); + + startSoapDocument(writer); + + writer.writeStartElement(ewsMsgNsUri, QStringLiteral("SyncFolderItems")); + + mShape.write(writer); + + writer.writeStartElement(ewsMsgNsUri, QStringLiteral("SyncFolderId")); + mFolderId.writeFolderIds(writer); + writer.writeEndElement(); + + if (!mSyncState.isNull()) { + writer.writeTextElement(ewsMsgNsUri, QStringLiteral("SyncState"), mSyncState); + } + + writer.writeTextElement(ewsMsgNsUri, QStringLiteral("MaxChangesReturned"), + QString::number(mMaxChanges)); + + writer.writeEndElement(); + + endSoapDocument(writer); + + qCDebug(EWSCLI_PROTO_LOG) << reqString; + + if (EWSCLI_REQUEST_LOG().isDebugEnabled()) { + QString st = mSyncState.isNull() ? QStringLiteral("none") : ewsHash(mSyncState); + QString folder; + qCDebugNCS(EWSCLI_REQUEST_LOG) << QStringLiteral("Starting SyncFolderItems request (folder: ") + << mFolderId << QStringLiteral(", state: %1").arg(st); + } + + prepare(reqString); + + doSend(); +} + +bool EwsSyncFolderItemsRequest::parseResult(QXmlStreamReader &reader) +{ + return parseResponseMessage(reader, QStringLiteral("SyncFolderItems"), + [this](QXmlStreamReader &reader) {return parseItemsResponse(reader);}); +} + +bool EwsSyncFolderItemsRequest::parseItemsResponse(QXmlStreamReader &reader) +{ + EwsSyncFolderItemsRequest::Response *resp = new EwsSyncFolderItemsRequest::Response(reader); + if (resp->responseClass() == EwsResponseUnknown) { + return false; + } + + mChanges = resp->mChanges; + mIncludesLastItem = resp->mIncludesLastItem; + mSyncState = resp->mSyncState; + + if (EWSCLI_REQUEST_LOG().isDebugEnabled()) { + if (resp->isSuccess()) { + qCDebugNC(EWSCLI_REQUEST_LOG) << QStringLiteral("Got SyncFolderItems response (%1 changes, last included: %2, state: %3)") + .arg(mChanges.size()).arg(mIncludesLastItem ? QStringLiteral("true") : QStringLiteral("false")) + .arg(qHash(mSyncState), 0, 36); + } else { + qCDebugNC(EWSCLI_REQUEST_LOG) << QStringLiteral("Got SyncFolderItems response - %1") + .arg(resp->responseMessage()); + } + } + + return true; +} + +EwsSyncFolderItemsRequest::Response::Response(QXmlStreamReader &reader) + : EwsRequest::Response(reader) +{ + if (mClass == EwsResponseParseError) { + return; + } + + static const QVector::Item> items = { + {SyncState, QStringLiteral("SyncState"), &ewsXmlTextReader}, + {IncludesLastItemInRange, QStringLiteral("IncludesLastItemInRange"), &ewsXmlBoolReader}, + {Changes, QStringLiteral("Changes"), &EwsSyncFolderItemsRequest::Response::changeReader}, + }; + static const EwsXml staticReader(items); + + EwsXml ewsReader(staticReader); + + if (!ewsReader.readItems(reader, ewsMsgNsUri, + [this](QXmlStreamReader &reader, const QString &) { + if (!readResponseElement(reader)) { + setErrorMsg(QStringLiteral("Failed to read EWS request - invalid response element.")); + return false; + } + return true; + })) + { + mClass = EwsResponseParseError; + return; + } + + QHash values = ewsReader.values(); + + mSyncState = values[SyncState].toString(); + mIncludesLastItem = values[IncludesLastItemInRange].toBool(); + mChanges = values[Changes].value(); +} + +bool EwsSyncFolderItemsRequest::Response::changeReader(QXmlStreamReader &reader, QVariant &val) +{ + Change::List changes; + QString elmName(reader.name().toString()); + + while (reader.readNextStartElement()) { + Change change(reader); + if (!change.isValid()) { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element").arg(elmName); + return false; + } + changes.append(change); + } + + val = QVariant::fromValue(changes); + return true; +} + +EwsSyncFolderItemsRequest::Change::Change(QXmlStreamReader &reader) +{ + static const QVector::Item> items = { + {Item, QStringLiteral("Item"), &ewsXmlItemReader}, + {Item, QStringLiteral("Message"), &ewsXmlItemReader}, + {Item, QStringLiteral("CalendarItem"), &ewsXmlItemReader}, + {Item, QStringLiteral("Contact"), &ewsXmlItemReader}, + {Item, QStringLiteral("DistributionList"), &ewsXmlItemReader}, + {Item, QStringLiteral("MeetingMessage"), &ewsXmlItemReader}, + {Item, QStringLiteral("MeetingRequest"), &ewsXmlItemReader}, + {Item, QStringLiteral("MeetingResponse"), &ewsXmlItemReader}, + {Item, QStringLiteral("MeetingCancellation"), &ewsXmlItemReader}, + {Item, QStringLiteral("Task"), &ewsXmlItemReader}, + {ItemId, QStringLiteral("ItemId"), &ewsXmlIdReader}, + {IsRead, QStringLiteral("IsRead"), &ewsXmlBoolReader} + }; + static const EwsXml staticReader(items); + + EwsXml ewsReader(staticReader); + + if (reader.name() == QStringLiteral("Create")) { + mType = Create; + } else if (reader.name() == QStringLiteral("Update")) { + mType = Update; + } else if (reader.name() == QStringLiteral("Delete")) { + mType = Delete; + } else if (reader.name() == QStringLiteral("ReadFlagChange")) { + mType = ReadFlagChange; + } + if (!ewsReader.readItems(reader, ewsTypeNsUri)) { + return; + } + + QHash values = ewsReader.values(); + + switch (mType) { + case Create: + case Update: + mItem = values[Item].value(); + mId = mItem[EwsItemFieldItemId].value(); + break; + case ReadFlagChange: + mIsRead = values[IsRead].toBool(); + /* fall through */ + case Delete: + mId = values[ItemId].value(); + break; + default: + break; + } +} diff --git a/resources/ews/ewsclient/ewstypes.h b/resources/ews/ewsclient/ewstypes.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewstypes.h @@ -0,0 +1,524 @@ +/* + 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 EWSTYPES_H +#define EWSTYPES_H + +#include +#include + +extern const QString soapEnvNsUri; +extern const QString ewsMsgNsUri; +extern const QString ewsTypeNsUri; + +typedef enum { + EwsFolderTypeMail = 0, + EwsFolderTypeCalendar, + EwsFolderTypeContacts, + EwsFolderTypeSearch, + EwsFolderTypeTasks, + EwsFolderTypeUnknown, +} EwsFolderType; + +typedef enum { + EwsResponseSuccess = 0, + EwsResponseWarning, + EwsResponseError, + EwsResponseParseError, // Internal - never returned by an Exchange server + EwsResponseUnknown // Internal - never returned by an Exchange server +} EwsResponseClass; + +typedef enum { + EwsDIdCalendar = 0, + EwsDIdContacts, + EwsDIdDeletedItems, + EwsDIdDrafts, + EwsDIdInbox, + EwsDIdJournal, + EwsDIdNotes, + EwsDIdOutbox, + EwsDIdSentItems, + EwsDIdTasks, + EwsDIdMsgFolderRoot, + EwsDIdRoot, + EwsDIdJunkEmail, + EwsDIdSearchFolders, + EwsDIdVoiceMail, + EwsDIdRecoverableItemsRoot, + EwsDIdRecoverableItemsDeletions, + EwsDIdRecoverableItemsVersions, + EwsDIdRecoverableItemsPurges, + EwsDIdArchiveRoot, + EwsDIdArchiveMsgFolderRoot, + EwsDIdArchiveDeletedItems, + EwsDIdArchiveRecoverableItemsRoot, + EwsDIdArchiveRecoverableItemsDeletions, + EwsDIdArchiveRecoverableItemsVersions, + EwsDIdArchiveRecoverableItemsPurges +} EwsDistinguishedId; + +typedef enum { + EwsShapeIdOnly = 0, + EwsShapeDefault, + EwsShapeAllProperties +} EwsBaseShape; + +typedef enum { + EwsPropSetMeeting = 0, + EwsPropSetAppointment, + EwsPropSetCommon, + EwsPropSetPublicStrings, + EwsPropSetAddress, + EwsPropSetInternetHeaders, + EwsPropSetCalendarAssistant, + EwsPropSetUnifiedMessaging +} EwsDistinguishedPropSetId; + +typedef enum { + EwsPropTypeApplicationTime = 0, + EwsPropTypeApplicationTimeArray, + EwsPropTypeBinary, + EwsPropTypeBinaryArray, + EwsPropTypeBoolean, + EwsPropTypeCLSID, + EwsPropTypeCLSIDArray, + EwsPropTypeCurrency, + EwsPropTypeCurrencyArray, + EwsPropTypeDouble, + EwsPropTypeDoubleArray, + EwsPropTypeError, + EwsPropTypeFloat, + EwsPropTypeFloatArray, + EwsPropTypeInteger, + EwsPropTypeTntegerArray, + EwsPropTypeLong, + EwsPropTypeLongArray, + EwsPropTypeNull, + EwsPropTypeObject, + EwsPropTypeObjectArray, + EwsPropTypeShort, + EwsPropTypeShortArray, + EwsPropTypeSystemTime, + EwsPropTypeSystemTimeArray, + EwsPropTypeString, + EwsPropTypeStringArray +} EwsPropertyType; + +typedef enum { + EwsTraversalShallow = 0, + EwsTraversalDeep, + EwsTraversalSoftDeleted, + EwsTraversalAssociated +} EwsTraversalType; + +typedef enum { + EwsItemTypeItem = 0, + EwsItemTypeMessage, + EwsItemTypeCalendarItem, + EwsItemTypeContact, + EwsItemTypeDistributionList, + EwsItemTypeMeetingMessage, + EwsItemTypeMeetingRequest, + EwsItemTypeMeetingResponse, + EwsItemTypeMeetingCancellation, + EwsItemTypeTask, + EwsItemTypeAbchPerson, + EwsItemTypePostItem, + EwsItemTypeUnknown +} EwsItemType; + +typedef enum { + EwsItemSensitivityNormal, + EwsItemSensitivityPersonal, + EwsItemSensitivityPrivate, + EwsItemSensitivityConfidential +} EwsItemSensitivity; + +/** + * @brief List of fields in EWS Item and its descendants + * + * The list is based on the XSD schema and contains duplicates, which were commented out. + */ +typedef enum { + // Folder + EwsFolderFieldFolderId, + EwsFolderFieldParentFolderId, + EwsFolderFieldFolderClass, + EwsFolderFieldDisplayName, + EwsFolderFieldTotalCount, + EwsFolderFieldChildFolderCount, + EwsFolderFieldManagedFolderInformation, + EwsFolderFieldEffectiveRights, + // Calendar folder + EwsFolderFieldPermissionSet, + // Contacts folder + //EwsFolderFieldPermissionSet, DUPLICATE + // Mail folder + EwsFolderFieldUnreadCount, + //EwsFolderFieldPermissionSet, DUPLICATE + // Search folder + //EwsFolderFieldUnreadCount, DUPLICATE + EwsFolderFieldSearchParameters, + // Tasks folder + //EwsFolderFieldUnreadCount, DUPLICATE + + // Item + EwsItemFieldMimeContent, + EwsItemFieldItemId, + EwsItemFieldParentFolderId, + EwsItemFieldItemClass, + EwsItemFieldSubject, + EwsItemFieldSensitivity, + EwsItemFieldBody, + EwsItemFieldAttachments, + EwsItemFieldDateTimeReceived, + EwsItemFieldSize, + EwsItemFieldCategories, + EwsItemFieldImportance, + EwsItemFieldInReplyTo, + EwsItemFieldIsSubmitted, + EwsItemFieldIsDraft, + EwsItemFieldIsFromMe, + EwsItemFieldIsResend, + EwsItemFieldIsUnmodified, + EwsItemFieldInternetMessageHeaders, + EwsItemFieldDateTimeSent, + EwsItemFieldDateTimeCreated, + EwsItemFieldResponseObjects, + EwsItemFieldReminderDueBy, + EwsItemFieldReminderIsSet, + EwsItemFieldReminderMinutesBeforeStart, + EwsItemFieldDisplayCc, + EwsItemFieldDisplayTo, + EwsItemFieldHasAttachments, + EwsItemFieldCulture, + EwsItemFieldEffectiveRights, + EwsItemFieldLastModifiedName, + EwsItemFieldLastModifiedTime, + EwsItemFieldIsAssociated, + EwsItemFieldWebClientReadFormQueryString, + EwsItemFieldWebClientEditFormQueryString, + EwsItemFieldConversationId, + EwsItemFieldUniqueBody, + EwsItemFieldFlag, + EwsItemFieldStoreEntryId, + EwsItemFieldInstanceKey, + EwsItemFieldNormalizedBody, + EwsItemFieldEntityExtractionResult, + EwsItemFieldPolicyTag, + EwsItemFieldArchiveTag, + EwsItemFieldRetentionDate, + EwsItemFieldPreview, + EwsItemFieldRightsManagementLicenseData, + EwsItemFieldPredictedActionReasons, + EwsItemFieldIsClutter, + EwsItemFieldBlockStatus, + EwsItemFieldHasBlockedImages, + EwsItemFieldTextBody, + EwsItemFieldIconIndex, + EwsItemFieldSearchKey, + EwsItemFieldSortKey, + EwsItemFieldHashtags, + EwsItemFieldMentions, + EwsItemFieldMentionedMe, + EwsItemFieldMentionsPreview, + EwsItemFieldMentionsEx, + EwsItemFieldAppliedHashtags, + EwsItemFieldAppliedHashtagsPreview, + EwsItemFieldLikes, + EwsItemFieldLikesPreview, + EwsItemFieldPendingSocialActivityTagIds, + EwsItemFieldAtAllMention, + EwsItemFieldCanDelete, + EwsItemFieldInferenceClassification, + // Message + EwsItemFieldSender, + EwsItemFieldToRecipients, + EwsItemFieldCcRecipients, + EwsItemFieldBccRecipients, + EwsItemFieldIsReadReceiptRequested, + EwsItemFieldIsDeliveryReceiptRequested, + EwsItemFieldConversationIndex, + EwsItemFieldConversationTopic, + EwsItemFieldFrom, + EwsItemFieldInternetMessageId, + EwsItemFieldIsRead, + EwsItemFieldIsResponseRequested, + EwsItemFieldReferences, + EwsItemFieldReplyTo, + EwsItemFieldReceivedBy, + EwsItemFieldReceivedRepresenting, + // Task + EwsItemFieldActualWork, + EwsItemFieldAssignedTime, + EwsItemFieldBillingInformation, + EwsItemFieldChangeCount, + EwsItemFieldCompanies, + EwsItemFieldCompleteDate, + EwsItemFieldContacts, + EwsItemFieldDelegationState, + EwsItemFieldDelegator, + EwsItemFieldDueDate, + EwsItemFieldIsAssignmentEditable, + EwsItemFieldIsComplete, + EwsItemFieldIsRecurring, + EwsItemFieldIsTeamTask, + EwsItemFieldMileage, + EwsItemFieldOwner, + EwsItemFieldPercentComplete, + EwsItemFieldRecurrence, + EwsItemFieldStartDate, + EwsItemFieldStatus, + EwsItemFieldStatusDescription, + EwsItemFieldTotalWork, + // Calendar + EwsItemFieldUID, + EwsItemFieldRecurrenceId, + EwsItemFieldDateTimeStamp, + EwsItemFieldStart, + EwsItemFieldEnd, + EwsItemFieldOriginalStart, + EwsItemFieldIsAllDayEvent, + EwsItemFieldLegacyFreeBusyStatus, + EwsItemFieldLocation, + EwsItemFieldWhen, + EwsItemFieldIsMeeting, + EwsItemFieldIsCancelled, + //EwsItemFieldIsRecurring, DUPLICATE + EwsItemFieldMeetingRequestWasSent, + //EwsItemFieldIsResponseRequested, DUPLICATE + EwsItemFieldCalendarItemType, + EwsItemFieldMyResponseType, + EwsItemFieldOrganizer, + EwsItemFieldRequiredAttendees, + EwsItemFieldOptionalAttendees, + EwsItemFieldResources, + EwsItemFieldConflictingMeetingCount, + EwsItemFieldAdjacentMeetingCount, + EwsItemFieldConflictingMeetings, + EwsItemFieldAdjacentMeetings, + EwsItemFieldDuration, + EwsItemFieldTimeZone, + EwsItemFieldStartTimeZone, + EwsItemFieldEndTimeZone, + EwsItemFieldAppointmentReplyTime, + EwsItemFieldAppointmentSequenceNumber, + EwsItemFieldAppointmentState, + //EwsItemFieldRecurrence, DUPLICATE + EwsItemFieldFirstOccurrence, + EwsItemFieldLastOccurrence, + EwsItemFieldModifiedOccurrences, + EwsItemFieldDeletedOccurrences, + EwsItemFieldMeetingTimeZone, + EwsItemFieldConferenceType, + EwsItemFieldAllowNewTimeProposal, + EwsItemFieldIsOnlineMeeting, + EwsItemFieldMeetingWorkspaceUrl, + EwsItemFieldNetShowUrl, + EwsItemFieldEnhancedLocation, + EwsItemFieldStartWallClock, + EwsItemFieldEndWallClock, + EwsItemFieldStartTimeZoneId, + EwsItemFieldEndTimeZoneId, + EwsItemFieldIntendedFreeBusyStatus, + EwsItemFieldJoinOnlineMeetingUrl, + EwsItemFieldOnlineMeetingSettings, + EwsItemFieldIsOrganizer, + EwsItemFieldCalendarActivityData, + EwsItemFieldDoNotForwardMeeting, + // MeetingMessage + EwsItemFieldAssociatedCalendarItemId, + EwsItemFieldIsDelegated, + EwsItemFieldIsOutOfDate, + EwsItemFieldHasBeenProcessed, + EwsItemFieldResponseType, + //EwsItemFieldUID, DUPLICATE + //EwsItemFieldRecurrenceId, DUPLICATE + //EwsItemFieldDateTimeStamp, DUPLICATE + // MeetingRequestMessage + EwsItemFieldMeetingRequestType, + //EwsItemFieldIntendedFreeBusyStatus, DUPLICATE + //EwsItemFieldStart, DUPLICATE + //EwsItemFieldEnd, DUPLICATE + //EwsItemFieldOriginalStart, DUPLICATE + //EwsItemFieldIsAllDayEvent, DUPLICATE + //EwsItemFieldLegacyFreeBusyStatus, DUPLICATE + //EwsItemFieldLocation, DUPLICATE + //EwsItemFieldWhen, DUPLICATE + //EwsItemFieldIsMeeting, DUPLICATE + //EwsItemFieldIsCancelled, DUPLICATE + //EwsItemFieldIsRecurring, DUPLICATE + //EwsItemFieldMeetingRequestWasSent, DUPLICATE + //EwsItemFieldCalendarItemType, DUPLICATE + //EwsItemFieldMyResponseType, DUPLICATE + //EwsItemFieldOrganizer, DUPLICATE + //EwsItemFieldRequiredAttendees, DUPLICATE + //EwsItemFieldOptionalAttendees, DUPLICATE + //EwsItemFieldResources, DUPLICATE + //EwsItemFieldConflictingMeetingCount, DUPLICATE + //EwsItemFieldAdjacentMeetingCount, DUPLICATE + //EwsItemFieldConflictingMeetings, DUPLICATE + //EwsItemFieldAdjacentMeetings, DUPLICATE + //EwsItemFieldDuration, DUPLICATE + //EwsItemFieldTimeZone, DUPLICATE + //EwsItemFieldAppointmentReplyTime, DUPLICATE + //EwsItemFieldAppointmentSequenceNumber,DUPLICATE + //EwsItemFieldAppointmentState, DUPLICATE + //EwsItemFieldRecurrence, DUPLICATE + //EwsItemFieldFirstOccurrence, DUPLICATE + //EwsItemFieldLastOccurrence, DUPLICATE + //EwsItemFieldModifiedOccurrences, DUPLICATE + //EwsItemFieldDeletedOccurrences, DUPLICATE + //EwsItemFieldMeetingTimeZone, DUPLICATE + //EwsItemFieldConferenceType, DUPLICATE + //EwsItemFieldAllowNewTimeProposal, DUPLICATE + //EwsItemFieldIsOnlineMeeting, DUPLICATE + //EwsItemFieldMeetingWorkspaceUrl, DUPLICATE + //EwsItemFieldNetShowUrl, DUPLICATE + // Contact + EwsItemFieldFileAs, + EwsItemFieldFileAsMapping, + EwsItemFieldDisplayName, + EwsItemFieldGivenName, + EwsItemFieldInitials, + EwsItemFieldMiddleName, + EwsItemFieldNickname, + EwsItemFieldCompleteName, + EwsItemFieldCompanyName, + EwsItemFieldEmailAddresses, + EwsItemFieldPhysicalAddresses, + EwsItemFieldPhoneNumbers, + EwsItemFieldAssistantName, + EwsItemFieldBirthday, + EwsItemFieldBusinessHomePage, + EwsItemFieldChildren, + //EwsItemFieldCompanies, DUPLICATE + EwsItemFieldContactSource, + EwsItemFieldDepartment, + EwsItemFieldGeneration, + EwsItemFieldImAddresses, + EwsItemFieldJobTitle, + EwsItemFieldManager, + //EwsItemFieldMileage, DUPLICATE + EwsItemFieldOfficeLocation, + EwsItemFieldPostalAddressIndex, + EwsItemFieldProfession, + EwsItemFieldSpouseName, + EwsItemFieldSurname, + EwsItemFieldWeddingAnniversary, + // DistributionList + //EwsItemFieldDisplayName, DUPLICATE + //EwsItemFieldFileAs, DUPLICATE + //EwsItemFieldContactSource, DUPLICATE + // Additional fields not in EWS specification + EwsItemFieldBodyIsHtml, + EwsItemFieldExtendedProperties, + EwsItemFieldExchangePersonIdGuid, +} EwsItemFields; + +typedef enum { + EwsItemImportanceLow, + EwsItemImportanceNormal, + EwsItemImportanceHigh +} EwsItemImportance; + +typedef enum { + EwsBasePointBeginning, + EwsBasePointEnd +} EwsIndexedViewBasePoint; + +typedef enum { + EwsCalendarItemSingle = 0, + EwsCalendarItemOccurrence, + EwsCalendarItemException, + EwsCalendarItemRecurringMaster +} EwsCalendarItemType; + +typedef enum { + EwsEventResponseUnknown = 0, + EwsEventResponseOrganizer, + EwsEventResponseTentative, + EwsEventResponseAccept, + EwsEventResponseDecline, + EwsEventResponseNotReceived +} EwsEventResponseType; + +typedef enum { + EwsLfbStatusFree = 0, + EwsLfbStatusTentative, + EwsLfbStatusBusy, + EwsLfbOutOfOffice, + EwsLfbNoData +} EwsLegacyFreeBusyStatus; + +typedef enum { + EwsDispSaveOnly = 0, + EwsDispSendOnly, + EwsDispSendAndSaveCopy, +} EwsMessageDisposition; + +typedef enum { + EwsResolNeverOverwrite = 0, + EwsResolAutoResolve, + EwsResolAlwaysOverwrite, +} EwsConflictResolution; + +typedef enum { + EwsMeetingDispSendToNone = 0, + EwsMeetingDispSendOnlyToAll, + EwsMeetingDispSendOnlyToChanged, + EwsMeetingDispSendToAllAndSaveCopy, + EwsMeetingDispSendToChangedAndSaveCopy, + EwsMeetingDispUnspecified +} EwsMeetingDisposition; + +typedef enum { + EwsCopiedEvent = 0, + EwsCreatedEvent, + EwsDeletedEvent, + EwsModifiedEvent, + EwsMovedEvent, + EwsNewMailEvent, + EwsFreeBusyChangedEvent, + EwsStatusEvent, + EwsUnknownEvent +} EwsEventType; + +template T decodeEnumString(const QString &str, const QString* table, unsigned count, bool *ok) +{ + unsigned i; + T enumVal = T(); + for (i = 0; i < count; i++) { + if (str == table[i]) { + enumVal = static_cast(i); + break; + } + } + *ok = (i < count); + return enumVal; +} + +inline bool isEwsMessageItemType(EwsItemType type) +{ + return (type == EwsItemTypeItem) || (type == EwsItemTypePostItem); +} + +extern const QVector ewsItemTypeNames; + +#endif diff --git a/resources/ews/ewsclient/ewstypes.cpp b/resources/ews/ewsclient/ewstypes.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewstypes.cpp @@ -0,0 +1,37 @@ +/* + 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 "ewstypes.h" + +const QString soapEnvNsUri = QStringLiteral("http://schemas.xmlsoap.org/soap/envelope/"); +const QString ewsMsgNsUri = QStringLiteral("http://schemas.microsoft.com/exchange/services/2006/messages"); +const QString ewsTypeNsUri = QStringLiteral("http://schemas.microsoft.com/exchange/services/2006/types"); + +const QVector ewsItemTypeNames = { + QStringLiteral("Item"), + QStringLiteral("Message"), + QStringLiteral("CalendarItem"), + QStringLiteral("Contact"), + QStringLiteral("DistributionList"), + QStringLiteral("MeetingMessage"), + QStringLiteral("MeetingRequest"), + QStringLiteral("MeetingResponse"), + QStringLiteral("MeetingCancellation"), + QStringLiteral("Task") +}; diff --git a/resources/ews/ewsclient/ewsunsubscriberequest.h b/resources/ews/ewsclient/ewsunsubscriberequest.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsunsubscriberequest.h @@ -0,0 +1,71 @@ +/* + 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 EWSUNSUBSCRIBEREQUEST_H +#define EWSUNSUBSCRIBEREQUEST_H + +#include + +#include "ewsid.h" +#include "ewsrequest.h" +#include "ewstypes.h" + +class EwsUnsubscribeRequest : public EwsRequest +{ + Q_OBJECT +public: + + enum Type { + PullSubscription = 0, + PushSubscription, + StreamingSubscription + }; + + class Response : public EwsRequest::Response + { + protected: + Response(QXmlStreamReader &reader); + + friend class EwsUnsubscribeRequest; + }; + + EwsUnsubscribeRequest(EwsClient &client, QObject *parent); + virtual ~EwsUnsubscribeRequest(); + + void setSubscriptionId(const QString &id) + { + mSubscriptionId = id; + }; + + const Response &response() const + { + return *mResponse; + }; + + virtual void start() override; +protected: + virtual bool parseResult(QXmlStreamReader &reader) override; + bool parseUnsubscribeResponse(QXmlStreamReader &reader); +private: + QString mSubscriptionId; + + QSharedPointer mResponse; +}; + +#endif diff --git a/resources/ews/ewsclient/ewsunsubscriberequest.cpp b/resources/ews/ewsclient/ewsunsubscriberequest.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsunsubscriberequest.cpp @@ -0,0 +1,93 @@ +/* + 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())); + return; + } + + if (!readResponseElement(reader)) { + setErrorMsg(QStringLiteral("Failed to read EWS request - invalid response element.")); + return; + } + } +} diff --git a/resources/ews/ewsclient/ewsupdatefolderrequest.h b/resources/ews/ewsclient/ewsupdatefolderrequest.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsupdatefolderrequest.h @@ -0,0 +1,129 @@ +/* + 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 EWSUPDATEFOLDERREQUEST_H +#define EWSUPDATEFOLDERREQUEST_H + +#include +#include + +#include "ewsfolder.h" +#include "ewsfoldershape.h" +#include "ewsrequest.h" +#include "ewstypes.h" + +class QXmlStreamReader; +class QXmlStreamWriter; + +class EwsUpdateFolderRequest : public EwsRequest +{ + Q_OBJECT +public: + + class Update + { + public: + bool write(QXmlStreamWriter &writer, EwsFolderType folderType) const; + protected: + enum Type { + Append = 0, + Set, + Delete, + Unknown + }; + + Update(EwsPropertyField field, const QVariant &val, Type type) + : mField(field), mValue(val), mType(type) {}; + + EwsPropertyField mField; + QVariant mValue; + Type mType; + }; + + class AppendUpdate : public Update + { + public: + AppendUpdate(EwsPropertyField field, const QVariant &val) : Update(field, val, Append) {}; + }; + + class SetUpdate : public Update + { + public: + SetUpdate(EwsPropertyField field, const QVariant &val) : Update(field, val, Set) {}; + }; + + class DeleteUpdate : public Update + { + public: + DeleteUpdate(EwsPropertyField field) : Update(field, QVariant(), Delete) {}; + }; + + class FolderChange + { + public: + FolderChange(EwsId folderId, EwsFolderType type) : mId(folderId), mType(type) {}; + void addUpdate(const Update *upd) + { + mUpdates.append(QSharedPointer(upd)); + } + bool write(QXmlStreamWriter &writer) const; + private: + EwsId mId; + EwsFolderType mType; + QList> mUpdates; + }; + + class Response : public EwsRequest::Response + { + public: + const EwsId &folderId() const + { + return mId; + }; + protected: + Response(QXmlStreamReader &reader); + + EwsId mId; + + friend class EwsUpdateFolderRequest; + }; + + EwsUpdateFolderRequest(EwsClient &client, QObject *parent); + virtual ~EwsUpdateFolderRequest(); + + void addFolderChange(const FolderChange &change) + { + mChanges.append(change); + }; + + virtual void start() override; + + const QList &responses() const + { + return mResponses; + }; +protected: + virtual bool parseResult(QXmlStreamReader &reader) override; + bool parseItemsResponse(QXmlStreamReader &reader); +private: + QList mChanges; + QList mResponses; +}; + +#endif diff --git a/resources/ews/ewsclient/ewsupdatefolderrequest.cpp b/resources/ews/ewsclient/ewsupdatefolderrequest.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsupdatefolderrequest.cpp @@ -0,0 +1,174 @@ +/* + 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 "ewsupdatefolderrequest.h" +#include "ewsclient_debug.h" + +static const QVector updateTypeElementNames = { + QStringLiteral("AppendToFolderField"), + QStringLiteral("SetFolderField"), + QStringLiteral("DeleteFolderField") +}; + +static const QVector folderTypeNames = { + QStringLiteral("Folder"), + QStringLiteral("CalendarFolder"), + QStringLiteral("ContactsFolder"), + QStringLiteral("SearchFolder"), + QStringLiteral("TasksFolder") +}; + +EwsUpdateFolderRequest::EwsUpdateFolderRequest(EwsClient &client, QObject *parent) + : EwsRequest(client, parent) +{ +} + +EwsUpdateFolderRequest::~EwsUpdateFolderRequest() +{ +} + +void EwsUpdateFolderRequest::start() +{ + QString reqString; + QXmlStreamWriter writer(&reqString); + + startSoapDocument(writer); + + writer.writeStartElement(ewsMsgNsUri, QStringLiteral("UpdateFolder")); + + writer.writeStartElement(ewsMsgNsUri, QStringLiteral("FolderChanges")); + Q_FOREACH (const FolderChange &ch, mChanges) { + ch.write(writer); + } + writer.writeEndElement(); + + writer.writeEndElement(); + + endSoapDocument(writer); + + qCDebugNC(EWSCLI_REQUEST_LOG) << QStringLiteral("Starting UpdateFolder request (%1 changes)") + .arg(mChanges.size()); + + qCDebug(EWSCLI_PROTO_LOG) << reqString; + + prepare(reqString); + + doSend(); +} + +bool EwsUpdateFolderRequest::parseResult(QXmlStreamReader &reader) +{ + return parseResponseMessage(reader, QStringLiteral("UpdateFolder"), + [this](QXmlStreamReader &reader) {return parseItemsResponse(reader);}); +} + +bool EwsUpdateFolderRequest::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 UpdateFolder response - OK"); + } else { + qCDebugNC(EWSCLI_REQUEST_LOG) << QStringLiteral("Got UpdateFolder response - %1") + .arg(resp.responseMessage()); + } + } + + mResponses.append(resp); + return true; +} + +EwsUpdateFolderRequest::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())); + return; + } + + if (reader.name() == QStringLiteral("Folders")) { + if (reader.readNextStartElement()) { + EwsFolder folder(reader); + if (!folder.isValid()) { + return; + } + mId = folder[EwsFolderFieldFolderId].value(); + } + + // Finish the Folders element. + reader.skipCurrentElement(); + } else if (!readResponseElement(reader)) { + setErrorMsg(QStringLiteral("Failed to read EWS request - invalid response element.")); + return; + } + } +} + +bool EwsUpdateFolderRequest::Update::write(QXmlStreamWriter &writer, EwsFolderType folderType) const +{ + bool retVal = true; + + writer.writeStartElement(ewsTypeNsUri, updateTypeElementNames[mType]); + + mField.write(writer); + + if (mType != Delete) { + writer.writeStartElement(ewsTypeNsUri, folderTypeNames[folderType]); + retVal = mField.writeWithValue(writer, mValue); + writer.writeEndElement(); + } + + writer.writeEndElement(); + + return retVal; +} + +bool EwsUpdateFolderRequest::FolderChange::write(QXmlStreamWriter &writer) const +{ + bool retVal = true; + + writer.writeStartElement(ewsTypeNsUri, QStringLiteral("FolderChange")); + + mId.writeFolderIds(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/ewsupdateitemrequest.h b/resources/ews/ewsclient/ewsupdateitemrequest.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsupdateitemrequest.h @@ -0,0 +1,154 @@ +/* + 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 EWSUPDATEITEMREQUEST_H +#define EWSUPDATEITEMREQUEST_H + +#include +#include + +#include "ewsitem.h" +#include "ewsitemshape.h" +#include "ewsrequest.h" +#include "ewstypes.h" + +class QXmlStreamReader; +class QXmlStreamWriter; + +class EwsUpdateItemRequest : public EwsRequest +{ + Q_OBJECT +public: + + class Update + { + public: + bool write(QXmlStreamWriter &writer, EwsItemType itemType) const; + protected: + enum Type { + Append = 0, + Set, + Delete, + Unknown + }; + + Update(EwsPropertyField field, const QVariant &val, Type type) + : mField(field), mValue(val), mType(type) {}; + + EwsPropertyField mField; + QVariant mValue; + Type mType; + }; + + class AppendUpdate : public Update + { + public: + AppendUpdate(EwsPropertyField field, const QVariant &val) : Update(field, val, Append) {}; + }; + + class SetUpdate : public Update + { + public: + SetUpdate(EwsPropertyField field, const QVariant &val) : Update(field, val, Set) {}; + }; + + class DeleteUpdate : public Update + { + public: + DeleteUpdate(EwsPropertyField field) : Update(field, QVariant(), Delete) {}; + }; + + class ItemChange + { + public: + ItemChange(EwsId itemId, EwsItemType type) : mId(itemId), mType(type) {}; + void addUpdate(const Update *upd) + { + mUpdates.append(QSharedPointer(upd)); + } + bool write(QXmlStreamWriter &writer) const; + private: + EwsId mId; + EwsItemType mType; + QList> mUpdates; + }; + + class Response : public EwsRequest::Response + { + public: + const EwsId &itemId() const + { + return mId; + }; + unsigned conflictCount() const + { + return mConflictCount; + }; + protected: + Response(QXmlStreamReader &reader); + + unsigned mConflictCount; + EwsId mId; + + friend class EwsUpdateItemRequest; + }; + + EwsUpdateItemRequest(EwsClient &client, QObject *parent); + virtual ~EwsUpdateItemRequest(); + + void addItemChange(const ItemChange &change) + { + mChanges.append(change); + }; + void setMessageDisposition(EwsMessageDisposition disp) + { + mMessageDisp = disp; + }; + void setConflictResolution(EwsConflictResolution resol) + { + mConflictResol = resol; + }; + void setMeetingDisposition(EwsMeetingDisposition disp) + { + mMeetingDisp = disp; + }; + void setSavedFolderId(const EwsId &id) + { + mSavedFolderId = id; + }; + + virtual void start() override; + + const QList &responses() const + { + return mResponses; + }; +protected: + virtual bool parseResult(QXmlStreamReader &reader) override; + bool parseItemsResponse(QXmlStreamReader &reader); +private: + QList mChanges; + EwsMessageDisposition mMessageDisp; + EwsConflictResolution mConflictResol; + EwsMeetingDisposition mMeetingDisp; + EwsId mSavedFolderId; + QList mResponses; +}; + +#endif diff --git a/resources/ews/ewsclient/ewsupdateitemrequest.cpp b/resources/ews/ewsclient/ewsupdateitemrequest.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsupdateitemrequest.cpp @@ -0,0 +1,226 @@ +/* + 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")); + Q_FOREACH (const ItemChange &ch, 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())); + return; + } + + if (reader.name() == QStringLiteral("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() == QStringLiteral("ConflictResults")) { + if (!reader.readNextStartElement()) { + setErrorMsg(QStringLiteral("Failed to read EWS request - expected a %1 element inside %2 element.") + .arg(QStringLiteral("Value")).arg(QStringLiteral("ConflictResults"))); + return; + } + + if (reader.name() != QStringLiteral("Count")) { + setErrorMsg(QStringLiteral("Failed to read EWS request - expected a %1 element inside %2 element.") + .arg(QStringLiteral("Count")).arg(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.h b/resources/ews/ewsclient/ewsxml.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsxml.h @@ -0,0 +1,177 @@ +/* + 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 EWSXMLREADER_H +#define EWSXMLREADER_H + +#include + +#include +#include + +#include "ewsclient_debug.h" + +template class EwsXml +{ +public: + typedef std::function ReadFunction; + typedef std::function WriteFunction; + typedef std::function UnknownElementFunction; + + typedef QHash ValueHash; + + static Q_CONSTEXPR T Ignore = static_cast(-1); + + struct Item { + Item() : key(Ignore) {}; + Item(T k, const QString &n, ReadFunction rfn = ReadFunction(), WriteFunction wfn = WriteFunction()) + : key(k), elmName(n), readFn(rfn), writeFn(wfn) {}; + T key; + QString elmName; + ReadFunction readFn; + WriteFunction writeFn; + }; + + EwsXml() {}; + EwsXml(const QVector &items) : mItems(items) + { + rebuildItemHash(); + }; + EwsXml(const EwsXml &other) + : mItems(other.mItems), mValues(other.mValues), mItemHash(other.mItemHash) {}; + + void setItems(const QVector &items) + { + mItems = items; + rebuildItemHash(); + }; + + bool readItem(QXmlStreamReader &reader, const QString &parentElm, const QString &nsUri, + UnknownElementFunction unknownElmFn = &defaultUnknownElmFunction) + { + typename QHash::iterator it = mItemHash.find(reader.name().toString()); + if (it != mItemHash.end() && nsUri == reader.namespaceUri()) { + if (it->key == Ignore) { + qCInfoNC(EWSCLI_LOG) << QStringLiteral("Unsupported %1 child element %2 - ignoring.") + .arg(parentElm).arg(reader.name().toString()); + reader.skipCurrentElement(); + return true; + } else if (!it->readFn) { + qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - no read support for %2 element.") + .arg(parentElm).arg(reader.name().toString()); + return false; + } else { + QVariant val = mValues[it->key]; + if (it->readFn(reader, val)) { + mValues[it->key] = val; + return true; + } + return false; + } + } + return unknownElmFn(reader, parentElm); + } + + bool readItems(QXmlStreamReader &reader, const QString &nsUri, + UnknownElementFunction unknownElmFn = &defaultUnknownElmFunction) + { + QString elmName(reader.name().toString()); + while (reader.readNextStartElement()) { + if (!readItem(reader, elmName, nsUri, unknownElmFn)) { + return false; + } + } + return true; + } + + bool writeItems(QXmlStreamWriter &writer, const QString &parentElm, const QString &nsUri, + const ValueHash &values, const QList &keysToWrite = QList()) const + { + bool hasKeysToWrite = !keysToWrite.isEmpty(); + Q_FOREACH (const Item &item, mItems) { + if (!hasKeysToWrite || keysToWrite.contains(item.key)) { + typename ValueHash::const_iterator it = values.find(item.key); + if (it != values.end()) { + if (!item.writeFn) { + qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to write %1 element - no write support for %2 element.") + .arg(parentElm).arg(item.elmName); + return false; + } + writer.writeStartElement(nsUri, item.elmName); + bool status = item.writeFn(writer, *it); + writer.writeEndElement(); + if (!status) { + return false; + } + } + } + } + return true; + } + + ValueHash values() const + { + return mValues; + } + +private: + static bool defaultUnknownElmFunction(QXmlStreamReader &reader, const QString &parentElm) + { + qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid %2 element.") + .arg(parentElm).arg(reader.name().toString()); + return false; + } + + const QVector mItems; + ValueHash mValues; + QHash mItemHash; + + void rebuildItemHash() + { + Q_FOREACH (const Item &item, mItems) { + mItemHash.insert(item.elmName, item); + } + } +}; + +template +T readXmlElementValue(QXmlStreamReader &reader, bool &ok, const QString &parentElement); + +extern bool ewsXmlBoolReader(QXmlStreamReader &reader, QVariant &val); +extern bool ewsXmlBoolWriter(QXmlStreamWriter &writer, const QVariant &val); +extern bool ewsXmlBase64Reader(QXmlStreamReader &reader, QVariant &val); +extern bool ewsXmlBase64Writer(QXmlStreamWriter &writer, const QVariant &val); +extern bool ewsXmlIdReader(QXmlStreamReader &reader, QVariant &val); +extern bool ewsXmlIdWriter(QXmlStreamWriter &writer, const QVariant &val); +extern bool ewsXmlTextReader(QXmlStreamReader &reader, QVariant &val); +extern bool ewsXmlTextWriter(QXmlStreamWriter &writer, const QVariant &val); +extern bool ewsXmlUIntReader(QXmlStreamReader &reader, QVariant &val); +extern bool ewsXmlUIntWriter(QXmlStreamWriter &writer, const QVariant &val); +extern bool ewsXmlDateTimeReader(QXmlStreamReader &reader, QVariant &val); +extern bool ewsXmlItemReader(QXmlStreamReader &reader, QVariant &val); +extern bool ewsXmlFolderReader(QXmlStreamReader &reader, QVariant &val); + +extern bool ewsXmlEnumReader(QXmlStreamReader &reader, QVariant &val, QVector items); +extern bool ewsXmlSensitivityReader(QXmlStreamReader &reader, QVariant &val); +extern bool ewsXmlImportanceReader(QXmlStreamReader &reader, QVariant &val); +extern bool ewsXmlCalendarItemTypeReader(QXmlStreamReader &reader, QVariant &val); +extern bool ewsXmlLegacyFreeBusyStatusReader(QXmlStreamReader &reader, QVariant &val); +extern bool ewsXmlResponseTypeReader(QXmlStreamReader &reader, QVariant &val); + +#endif diff --git a/resources/ews/ewsclient/ewsxml.cpp b/resources/ews/ewsclient/ewsxml.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsclient/ewsxml.cpp @@ -0,0 +1,382 @@ +/* + 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) +{ + 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 == QStringLiteral("true")) { + val = true; + } else if (elmText == QStringLiteral("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) +{ + 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, QVector items) +{ + 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); + 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()); + 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()); + } + } + + 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()); + } + } + + 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()); + 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 == QStringLiteral("true")) { + val = true; + } else if (valStr == QStringLiteral("false")) { + val = false; + } else { + qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid %2 element.") + .arg(parentElement).arg(elmName.toString()); + ok = false; + } + } + + return val; +} + +template <> +QByteArray readXmlElementValue(QXmlStreamReader &reader, bool &ok, const QString &parentElement) +{ + QStringRef elmName = reader.name(); + 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.toAscii()); + } + + return val; +} + + diff --git a/resources/ews/ewscreateitemjob.h b/resources/ews/ewscreateitemjob.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewscreateitemjob.h @@ -0,0 +1,57 @@ +/* + 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. +*/ + +#ifndef EWSCREATEITEMJOB_H +#define EWSCREATEITEMJOB_H + +#include +#include +#include "ewsjob.h" + +class EwsClient; +class EwsItem; +class EwsTagStore; +class EwsResource; + +class EwsCreateItemJob : public EwsJob +{ + Q_OBJECT +public: + EwsCreateItemJob(EwsClient &client, const Akonadi::Item &item, + const Akonadi::Collection &collection, EwsTagStore *tagStore, EwsResource *parent); + virtual ~EwsCreateItemJob(); + + virtual bool setSend(bool send = true) = 0; + + const Akonadi::Item &item() const; + + void start() override; +private Q_SLOTS: + void tagSyncFinished(KJob *job); +protected: + void populateCommonProperties(EwsItem &item); + virtual void doStart() = 0; + + Akonadi::Item mItem; + Akonadi::Collection mCollection; + EwsClient &mClient; + EwsTagStore *mTagStore; +}; + +#endif diff --git a/resources/ews/ewscreateitemjob.cpp b/resources/ews/ewscreateitemjob.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewscreateitemjob.cpp @@ -0,0 +1,78 @@ +/* + 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 "ewscreateitemjob.h" + +#include "ewsresource.h" +#include "tags/ewstagstore.h" +#include "tags/ewsakonaditagssyncjob.h" + +EwsCreateItemJob::EwsCreateItemJob(EwsClient &client, const Akonadi::Item &item, + const Akonadi::Collection &collection, EwsTagStore *tagStore, + EwsResource *parent) + : EwsJob(parent), mItem(item), mCollection(collection), mClient(client), mTagStore(tagStore) +{ +} + +EwsCreateItemJob::~EwsCreateItemJob() +{ +} + +const Akonadi::Item &EwsCreateItemJob::item() const +{ + return mItem; +} + +void EwsCreateItemJob::start() +{ + /* Before starting check if all Akonadi tags are known to the tag store */ + bool syncNeeded = false; + Q_FOREACH (const Akonadi::Tag &tag, mItem.tags()) { + if (!mTagStore->containsId(tag.id())) { + syncNeeded = true; + break; + } + } + + if (syncNeeded) { + EwsAkonadiTagsSyncJob *job = new EwsAkonadiTagsSyncJob(mTagStore, + mClient, qobject_cast(parent())->rootCollection(), this); + connect(job, &EwsAkonadiTagsSyncJob::result, this, &EwsCreateItemJob::tagSyncFinished); + job->start(); + } else { + doStart(); + } +} + +void EwsCreateItemJob::populateCommonProperties(EwsItem &item) +{ + if (!mTagStore->writeEwsProperties(mItem, item)) { + setErrorMsg(QStringLiteral("Failed to write tags despite an earlier sync")); + } +} + +void EwsCreateItemJob::tagSyncFinished(KJob *job) +{ + if (job->error()) { + setErrorMsg(job->errorText()); + emitResult(); + } else { + doStart(); + } +} diff --git a/resources/ews/ewsfetchfoldersincrjob.h b/resources/ews/ewsfetchfoldersincrjob.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsfetchfoldersincrjob.h @@ -0,0 +1,68 @@ +/* + 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. +*/ + +#ifndef EWSFETCHFOLDERSINCRJOB_H +#define EWSFETCHFOLDERSINCRJOB_H + +#include + +#include + +#include "ewsjob.h" +#include "ewsfolder.h" + +class EwsClient; +class EwsFetchFoldersIncrJobPrivate; + +class EwsFetchFoldersIncrJob : public EwsJob +{ + Q_OBJECT +public: + EwsFetchFoldersIncrJob(EwsClient &client, const QString &syncState, + const Akonadi::Collection &rootCollection, QObject *parent); + virtual ~EwsFetchFoldersIncrJob(); + + Akonadi::Collection::List changedFolders() const + { + return mChangedFolders; + }; + Akonadi::Collection::List deletedFolders() const + { + return mDeletedFolders; + }; + const QString &syncState() const + { + return mSyncState; + }; + + virtual void start() override; +Q_SIGNALS: + void status(int status, const QString &message = QString()); + void percent(int progress); +private: + Akonadi::Collection::List mChangedFolders; + Akonadi::Collection::List mDeletedFolders; + + QString mSyncState; + + QScopedPointer d_ptr; + Q_DECLARE_PRIVATE(EwsFetchFoldersIncrJob) +}; + +#endif diff --git a/resources/ews/ewsfetchfoldersincrjob.cpp b/resources/ews/ewsfetchfoldersincrjob.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsfetchfoldersincrjob.cpp @@ -0,0 +1,584 @@ +/* + 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 "ewsfetchfoldersincrjob.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ewssyncfolderhierarchyrequest.h" +#include "ewsgetfolderrequest.h" +#include "ewseffectiverights.h" +#include "ewsclient.h" +#include "ewsresource_debug.h" + +using namespace Akonadi; + +/* + * Performing an incremental folder tree update relies on the SyncFolderHierarchy EWS request, + * which returns a list of changes to the folder tree. Each of the change can be a folder creation, + * modification or removal notification. + * + * The EwsFetchFoldersIncrJob class starts by executing the SyncFolderHierarchy request in order to + * retrieve the remote changes. + * + * Once that is completed the remoteFolderIncrFetchDone() method processes the changes. For each + * one a folder descriptor (FolderDescr) is created and inserted into the folder hash (mFolderHash) + * keyed by the folder EWS identifier. The folder hash contains all collections that are being + * processed during the update. The flags member is used to determine the type of each collection. + * For each change the corresponding local collection needs to be retrieved in order to correctly + * pass the change list to Akonadi. The following rules apply: + * + * * For created folders the parent collection is retrieved. This is necessary to put a valid parent + * collection to the newly created one. In order to handle cascaded folder creations + * (i.e. two folders are created, one child of the other) the parent collection is only retrieved + * for the topmost created folder. + * * For updated/modified folders both the current (corresponding to the EWS updated folder) and + * parent collections are retrieved. The current collection is retrieved to update only the + * changed information in the collection. The parent collection is retrieved in order to detect + * and handle collection moves as in such case the Akonadi-side parent will not be the same as + * the EWS-side parent (the parent retrieved is the EWS-side parent and the Akonadi-side parent + * will be known as part of the current collection once retrieved from Akonadi). + * * For deleted folders the current (corresponding to the EWS deleted folder) is retrieved. + * + * After the local Akonadi collections are retrieved the objects are put into their corresponding + * folder descriptors in the folder hash. + * + * Having information about all remote changed folders and their local Akonadi collections the main + * part of the synchronization process can be started. + * + * In the first pass the processRemoteFolders() method looks at all folders in the hash table. + * Setting the parent-child relationships is performed at this stage only when the relevant parent + * collection has already been processed (the FolderDescr::Processed flag is set). This ensures + * that the parent-child relationships are set in the down-the-tree order. In case this condition + * is not met for a collection the need for an extra reparenting pass is flagged and the parent + * collection setting is not performed. + * + * Two types of folders are of main interest: + * + * * For created folders the Akonadi collection object is created and populated with data obtained + * from Exchange. If the parent collection has alredy been processed in this pass the parent is + * set on the newly created collection. + * * For modified folders the Akonadi collection object that was retrieved previously is updated + * with data obtained from Exchange. If the folder was moved (the Akonadi parent differs from the + * Exchange parent) a collection move is attempted. This needs to be done explicitly using a + * CollectionMoveJob as Akonadi is unable to detect collection moves in the sync code. Similar + * to the created folder case the move is only performed in case the new parent is flagged as + * processed. Additionally the code checks if the new parent is a newly created folder. In such + * case the whole incremental sync is aborted as handling this rare corner case would introduce + * extra complexity. In case of incremental sync failure the resource will fallback to a full + * sync that will handle the case. + * + * Regardless of collection type the first pass also builds a list of top-level collections + * (i.e. ones for which the parent is not in the folder hash) and a hash containing the parent-child + * relationship. Both lists will be needed in case a reparenting pass is needed. + * + * The optional reparenting pass follows the first pass. It is performed if processing of at least + * one collection failed due to an unprocessed parent. The reparenting pass focuses on the top-level + * folders and starting from each recursively goes into its children setting their parent to itself. + * The pass also processes any delayed collection moves in case executing them was impossible in the + * first pass. + * + * The final stage of the synchronization process builds a list of changed and deleted collections + * for Akonadi. At this stage all collections must be processed, otherwise an error is raised. If + * no collection moves have been executed the job is completed. Otherwise the completion is + * singalled once all moves are done. + */ + +static const EwsPropertyField propPidTagContainerClass(0x3613, EwsPropTypeString); + +class FolderDescr +{ +public: + typedef enum { + RemoteCreated = 0x0001, + RemoteUpdated = 0x0002, + RemoteDeleted = 0x0004, + Processed = 0x0008 + } Flag; + Q_DECLARE_FLAGS(Flags, Flag) + + FolderDescr() {}; + + Akonadi::Collection collection; + Flags flags; + EwsFolder ewsFolder; + + bool isCreated() const + { + return flags & RemoteCreated; + }; + bool isModified() const + { + return flags & RemoteUpdated; + }; + bool isRemoved() const + { + return flags & RemoteDeleted; + }; + bool isProcessed() const + { + return flags & Processed; + }; + QString parent() const + { + return ewsFolder.isValid() ? ewsFolder[EwsFolderFieldParentFolderId].value().id() : QString(); + }; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(FolderDescr::Flags) + +class EwsFetchFoldersIncrJobPrivate : public QObject +{ +public: + EwsFetchFoldersIncrJobPrivate(EwsFetchFoldersIncrJob *parent, EwsClient &client, + const Collection &rootCollection); + ~EwsFetchFoldersIncrJobPrivate(); + + bool processRemoteFolders(); + void updateFolderCollection(Collection &collection, const EwsFolder &folder); + + void reparentRemoteFolder(const QString &id); + void moveCollection(const FolderDescr &fd); +public Q_SLOTS: + void remoteFolderIncrFetchDone(KJob *job); + void localFolderFetchDone(KJob *job); + void localFolderMoveDone(KJob *job); +public: + EwsClient &mClient; + int mPendingMoveJobs; + EwsId::List mRemoteFolderIds; + + const Collection &mRootCollection; + + QMultiHash mParentMap; + + QHash mFolderHash; + + EwsFetchFoldersIncrJob *q_ptr; + Q_DECLARE_PUBLIC(EwsFetchFoldersIncrJob) +}; + +EwsFetchFoldersIncrJobPrivate::EwsFetchFoldersIncrJobPrivate(EwsFetchFoldersIncrJob *parent, EwsClient &client, + const Collection &rootCollection) + : QObject(parent), mClient(client), mRootCollection(rootCollection), q_ptr(parent) +{ + mPendingMoveJobs = 0; +} + +EwsFetchFoldersIncrJobPrivate::~EwsFetchFoldersIncrJobPrivate() +{ +} + +void EwsFetchFoldersIncrJobPrivate::remoteFolderIncrFetchDone(KJob *job) +{ + Q_Q(EwsFetchFoldersIncrJob); + + EwsSyncFolderHierarchyRequest *req = qobject_cast(job); + if (!req) { + qCWarning(EWSRES_LOG) << QStringLiteral("Invalid EwsSyncFolderHierarchyRequestjob object"); + q->setErrorMsg(QStringLiteral("Invalid EwsSyncFolderHierarchyRequest job object")); + q->emitResult(); + return; + } + + if (req->error()) { + return; + } + + if (req->changes().isEmpty()) { + /* Nothing to do. */ + q->emitResult(); + return; + } + + /* Build a list of local collections to fetch in response to the remote changes. + * Use a hash to auto-eliminate duplicates. */ + QHash localFetchHash; + + Q_FOREACH (const EwsSyncFolderHierarchyRequest::Change &ch, req->changes()) { + FolderDescr fd; + Collection c; + + switch (ch.type()) { + case EwsSyncFolderHierarchyRequest::Update: { + fd.ewsFolder = ch.folder(); + fd.flags |= FolderDescr::RemoteUpdated; + EwsId id = fd.ewsFolder[EwsFolderFieldFolderId].value(); + mFolderHash.insert(id.id(), fd); + + /* For updated folders fetch the collection corresponding to that folder and its parent + * (the parent will be needed in case of a collection move) */ + Collection c2; + c2.setRemoteId(fd.parent()); + localFetchHash.insert(c2.remoteId(), c2); + + c.setRemoteId(id.id()); + localFetchHash.insert(c.remoteId(), c); + break; + } + case EwsSyncFolderHierarchyRequest::Create: { + fd.ewsFolder = ch.folder(); + fd.flags |= FolderDescr::RemoteCreated; + EwsId id = fd.ewsFolder[EwsFolderFieldFolderId].value(); + mFolderHash.insert(id.id(), fd); + + c.setRemoteId(fd.parent()); + /* For created folders fetch the parent collection on Exchange side. Don't do this + * when the parent collection has also been created as it would fail. */ + if (!mFolderHash.value(fd.parent()).isCreated()) { + localFetchHash.insert(c.remoteId(), c); + } + break; + } + case EwsSyncFolderHierarchyRequest::Delete: { + fd.flags |= FolderDescr::RemoteDeleted; + mFolderHash.insert(ch.folderId().id(), fd); + + /* For deleted folders fetch the collection corresponding to the deleted folder. */ + c.setRemoteId(ch.folderId().id()); + localFetchHash.insert(c.remoteId(), c); + break; + } + default: + break; + } + } + + if (localFetchHash.isEmpty()) { + /* In either case at least one folder is expected to be queued for fetching. */ + q->setErrorMsg(QStringLiteral("Expected at least one local folder to fetch.")); + q->emitResult(); + return; + } + + q->mSyncState = req->syncState(); + + CollectionFetchJob *fetchJob = new CollectionFetchJob(localFetchHash.values().toVector(), + CollectionFetchJob::Base); + CollectionFetchScope scope; + scope.setAncestorRetrieval(CollectionFetchScope::All); + fetchJob->setFetchScope(scope); + connect(fetchJob, &CollectionFetchJob::result, this, + &EwsFetchFoldersIncrJobPrivate::localFolderFetchDone); + q->addSubjob(fetchJob); +} + +void EwsFetchFoldersIncrJobPrivate::localFolderFetchDone(KJob *job) +{ + Q_Q(EwsFetchFoldersIncrJob); + + if (job->error()) { + q->setErrorMsg(QStringLiteral("Failed to fetch local collections.")); + q->emitResult(); + return; + } + + CollectionFetchJob *fetchJob = qobject_cast(job); + Q_ASSERT(fetchJob); + + Q_FOREACH (const Collection &col, fetchJob->collections()) { + /* Retrieve the folder descriptor for this collection. Note that a new descriptor will be + * created if it does not yet exist. */ + FolderDescr &fd = mFolderHash[col.remoteId()]; + fd.collection = col; + if (!fd.flags) { + /* This collection has just been created and this means that it's a parent collection + * added in response to a created folder. Since the collection is here just for reference + * it will not be processed by processRemoteFolders() and can be marked accordingly. */ + fd.flags |= FolderDescr::Processed; + } + } + + if (!processRemoteFolders()) { + q->setErrorMsg(QStringLiteral("Failed to process remote folder list.")); + q->emitResult(); + } + + if (!mPendingMoveJobs) { + q->emitResult(); + } + /* Otherwise wait for the move requests to finish. */ +} + +bool EwsFetchFoldersIncrJobPrivate::processRemoteFolders() +{ + Q_Q(EwsFetchFoldersIncrJob); + + /* The list of top-level collections. It contains identifiers of collections for which the + * parent collection is not in the folder hash. This list is used at a later stage when + * setting collections parents. Building a top-level list is necessary as those updates can + * only be safely performed down the tree. */ + QStringList topLevelList; + + bool reparentPassNeeded = false; + + /* Iterate over all changed folders. */ + for (auto it = mFolderHash.begin(); it != mFolderHash.end(); ++it) { + qCDebugNC(EWSRES_LOG) << QStringLiteral("Processing: ") << it.key(); + + if (it->isModified()) { + qCDebugNC(EWSRES_LOG) << QStringLiteral("Collection was modified"); + updateFolderCollection(it->collection, it->ewsFolder); + + if (it->parent() != it->collection.parentCollection().remoteId()) { + /* This collection has been moved. Since Akonadi currently cannot handle collection + * moves the resource needs to manually move it. */ + qCDebugNC(EWSRES_LOG) << QStringLiteral("Collection was moved"); + + /* Before moving check if the parent exists and has been processed. */ + auto parentIt = mFolderHash.find(it->parent()); + if (parentIt == mFolderHash.end()) { + q->setErrorMsg(QStringLiteral("Found moved collection without new parent.")); + return false; + } + + if (parentIt->isCreated()) { + /* Further workarounds could be done here to ensure that the parent is manually + * created before triggering a move but this would just unnecessarily complicate + * matters. Instead just surrender and retry with a full sync. */ + q->setErrorMsg(QStringLiteral("Found moved collection to a just created parent.")); + return false; + } + + if (!parentIt->isProcessed()) { + qCDebugNC(EWSRES_LOG) << QStringLiteral("Parent not yet processed - delaying"); + /* The new parent collection is not yet processed - defer the move to make + * sure all the oprtations are done in down-the-tree order. */ + reparentPassNeeded = true; + } else { + moveCollection(*it); + it->collection.setParentCollection(parentIt->collection); + it->flags |= FolderDescr::Processed; + } + } else { + /* No collection move happening so nothing else to for this one. */ + it->flags |= FolderDescr::Processed; + } + } else if (it->isCreated()) { + qCDebugNC(EWSRES_LOG) << QStringLiteral("Collection was created"); + it->collection.setRemoteId(it.key()); + updateFolderCollection(it->collection, it->ewsFolder); + + auto parentIt = mFolderHash.find(it->parent()); + if (parentIt == mFolderHash.end()) { + q->setErrorMsg(QStringLiteral("Found created collection without parent.")); + return false; + } + + /* Check if the parent has already been processed. If yes, set the parent of this + * collection and mark this one as done. Otherwise a second pass will be needed later. */ + if (parentIt->isProcessed()) { + qCDebugNC(EWSRES_LOG) << QStringLiteral("Processing"); + it->collection.setParentCollection(parentIt->collection); + it->flags |= FolderDescr::Processed; + } else { + qCDebugNC(EWSRES_LOG) << QStringLiteral("Parent not yet processed - delaying"); + reparentPassNeeded = true; + } + } else { + qCDebugNC(EWSRES_LOG) << QStringLiteral("Collection is not remotely changed"); + /* This is either a deleted folder or a parent to an added collection. No processing + * needed for either of those. */ + it->flags |= FolderDescr::Processed; + } + + /* Check if this collection is a top-level collection. */ + if (!mFolderHash.contains(it->parent())) { + qCDebugNC(EWSRES_LOG) << QStringLiteral("Collection is top level"); + topLevelList.append(it.key()); + } + + /* Put the collection into the parent map. This will help running the reparent pass. */ + if (!it->parent().isNull()) { + mParentMap.insert(it->parent(), it.key()); + } + } + + if (reparentPassNeeded) { + qCDebugNC(EWSRES_LOG) << QStringLiteral("Executing reparent pass") << topLevelList; + Q_FOREACH (const QString &id, topLevelList) { + reparentRemoteFolder(id); + } + } + + /* Build the resulting collection list. */ + for (auto it = mFolderHash.begin(); it != mFolderHash.end(); ++it) { + if (it->isRemoved()) { + q->mDeletedFolders.append(it->collection); + } else if (it->isProcessed()) { + q->mChangedFolders.append(it->collection); + } else { + qCWarningNC(EWSRES_LOG) << QStringLiteral("Found unprocessed collection %1").arg(it.key()); + return false; + } + } + + return true; +} + +void EwsFetchFoldersIncrJobPrivate::reparentRemoteFolder(const QString &id) +{ + qCDebugNC(EWSRES_LOG) << QStringLiteral("Reparenting") << id; + QStringList children = mParentMap.values(id); + FolderDescr &fd = mFolderHash[id]; + Q_FOREACH (const QString &childId, children) { + FolderDescr &childFd = mFolderHash[childId]; + if (!childFd.isProcessed() && childFd.isModified() && + childFd.parent() != childFd.collection.parentCollection().remoteId()) { + qCDebugNC(EWSRES_LOG) << QStringLiteral("Found moved collection"); + /* Found unprocessed collection move. */ + moveCollection(childFd); + } + + childFd.collection.setParentCollection(fd.collection); + reparentRemoteFolder(childId); + } + fd.flags |= FolderDescr::Processed; +} + +void EwsFetchFoldersIncrJobPrivate::moveCollection(const FolderDescr &fd) +{ + qCDebugNC(EWSRES_LOG) << QStringLiteral("Moving collection") << fd.collection.remoteId() << + QStringLiteral("from") << fd.collection.parentCollection().remoteId() << + QStringLiteral("to") << fd.parent(); + CollectionMoveJob *job = new CollectionMoveJob(fd.collection, mFolderHash[fd.parent()].collection); + connect(job, &CollectionMoveJob::result, this, &EwsFetchFoldersIncrJobPrivate::localFolderMoveDone); + mPendingMoveJobs++; + job->start(); +} + +void EwsFetchFoldersIncrJobPrivate::localFolderMoveDone(KJob *job) +{ + Q_Q(EwsFetchFoldersIncrJob); + + if (job->error()) { + q->setErrorMsg(QStringLiteral("Failed to move collection.")); + q->emitResult(); + return; + } + + if (--mPendingMoveJobs == 0) { + q->emitResult(); + } +} + +void EwsFetchFoldersIncrJobPrivate::updateFolderCollection(Collection &collection, const EwsFolder &folder) +{ + collection.setName(folder[EwsFolderFieldDisplayName].toString()); + QStringList mimeTypes; + QString contClass = folder[propPidTagContainerClass].toString(); + mimeTypes.append(Collection::mimeType()); + switch (folder.type()) { + case EwsFolderTypeCalendar: + mimeTypes.append(KCalCore::Event::eventMimeType()); + break; + case EwsFolderTypeContacts: + mimeTypes.append(KContacts::Addressee::mimeType()); + mimeTypes.append(KContacts::ContactGroup::mimeType()); + break; + case EwsFolderTypeTasks: + mimeTypes.append(KCalCore::Todo::todoMimeType()); + break; + case EwsFolderTypeMail: + if (contClass == QStringLiteral("IPF.Note") || contClass.isEmpty()) { + mimeTypes.append(KMime::Message::mimeType()); + } + break; + default: + break; + } + collection.setContentMimeTypes(mimeTypes); + Collection::Rights colRights; + EwsEffectiveRights ewsRights = folder[EwsFolderFieldEffectiveRights].value(); + // FIXME: For now full read/write support is only implemented for e-mail. In order to avoid + // potential problems block write access to all other folder types. + if (folder.type() == EwsFolderTypeMail) { + if (ewsRights.canDelete()) { + colRights |= Collection::CanDeleteCollection | Collection::CanDeleteItem; + } + if (ewsRights.canModify()) { + colRights |= Collection::CanChangeCollection | Collection::CanChangeItem; + } + if (ewsRights.canCreateContents()) { + colRights |= Collection::CanCreateItem; + } + if (ewsRights.canCreateHierarchy()) { + colRights |= Collection::CanCreateCollection; + } + } + collection.setRights(colRights); + EwsId id = folder[EwsFolderFieldFolderId].value(); + collection.setRemoteRevision(id.changeKey()); +} + + +EwsFetchFoldersIncrJob::EwsFetchFoldersIncrJob(EwsClient &client, const QString &syncState, + const Akonadi::Collection &rootCollection, QObject *parent) + : EwsJob(parent), mSyncState(syncState), + d_ptr(new EwsFetchFoldersIncrJobPrivate(this, client, rootCollection)) +{ + qRegisterMetaType(); +} + +EwsFetchFoldersIncrJob::~EwsFetchFoldersIncrJob() +{ +} + +void EwsFetchFoldersIncrJob::start() +{ + Q_D(const EwsFetchFoldersIncrJob); + + EwsSyncFolderHierarchyRequest *syncFoldersReq = new EwsSyncFolderHierarchyRequest(d->mClient, this); + syncFoldersReq->setFolderId(EwsId(EwsDIdMsgFolderRoot)); + EwsFolderShape shape; + shape << propPidTagContainerClass; + shape << EwsPropertyField(QStringLiteral("folder:EffectiveRights")); + shape << EwsPropertyField(QStringLiteral("folder:ParentFolderId")); + syncFoldersReq->setFolderShape(shape); + if (!mSyncState.isNull()) { + syncFoldersReq->setSyncState(mSyncState); + } + connect(syncFoldersReq, &EwsSyncFolderHierarchyRequest::result, d, + &EwsFetchFoldersIncrJobPrivate::remoteFolderIncrFetchDone); + // Don't add this as a subjob as the error is handled in its own way rather than throwing an + // error code to the parent. + + syncFoldersReq->start(); +} + +QDebug operator<<(QDebug debug, const FolderDescr &fd) +{ + QDebugStateSaver saver(debug); + QDebug d = debug.nospace().noquote(); + d << QStringLiteral("FolderDescr("); + + d << fd.collection; + d << fd.flags; + + d << ')'; + return debug; +} diff --git a/resources/ews/ewsfetchfoldersjob.h b/resources/ews/ewsfetchfoldersjob.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsfetchfoldersjob.h @@ -0,0 +1,62 @@ +/* + 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. +*/ + +#ifndef EWSFETCHFOLDERSJOB_H +#define EWSFETCHFOLDERSJOB_H + +#include + +#include + +#include "ewsjob.h" +#include "ewsfolder.h" + +class EwsClient; +class EwsFetchFoldersJobPrivate; + +class EwsFetchFoldersJob : public EwsJob +{ + Q_OBJECT +public: + EwsFetchFoldersJob(EwsClient &client, const Akonadi::Collection &rootCollection, QObject *parent); + virtual ~EwsFetchFoldersJob(); + + Akonadi::Collection::List folders() const + { + return mFolders; + }; + const QString &syncState() const + { + return mSyncState; + }; + + virtual void start() override; +Q_SIGNALS: + void status(int status, const QString &message = QString()); + void percent(int progress); +private: + Akonadi::Collection::List mFolders; + + QString mSyncState; + + QScopedPointer d_ptr; + Q_DECLARE_PRIVATE(EwsFetchFoldersJob) +}; + +#endif diff --git a/resources/ews/ewsfetchfoldersjob.cpp b/resources/ews/ewsfetchfoldersjob.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsfetchfoldersjob.cpp @@ -0,0 +1,386 @@ +/* + 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 "ewsfetchfoldersjob.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ewssyncfolderhierarchyrequest.h" +#include "ewsgetfolderrequest.h" +#include "ewseffectiverights.h" +#include "ewsclient.h" +#include "ewsresource_debug.h" + +using namespace Akonadi; + +static const EwsPropertyField propPidTagContainerClass(0x3613, EwsPropTypeString); + +static Q_CONSTEXPR int fetchBatchSize = 50; + +class EwsFetchFoldersJobPrivate : public QObject +{ +public: + EwsFetchFoldersJobPrivate(EwsFetchFoldersJob *parent, EwsClient &client, + const Collection &rootCollection); + ~EwsFetchFoldersJobPrivate(); + + void processRemoteFolders(); + Collection createFolderCollection(const EwsFolder &folder); + + void buildCollectionList(); + void buildChildCollectionList(const Collection &col); +public Q_SLOTS: + void remoteFolderFullFetchDone(KJob *job); + void remoteFolderIdFullFetchDone(KJob *job); + void remoteFolderDetailFetchDone(KJob *job); +public: + EwsClient &mClient; + int mPendingFetchJobs; + int mPendingMoveJobs; + EwsId::List mRemoteFolderIds; + + const Collection &mRootCollection; + + EwsFolder::List mRemoteChangedFolders; // Contains details of folders that need update + // (either created or changed) + QHash mCollectionMap; + QMultiHash mParentMap; + + EwsFetchFoldersJob *q_ptr; + Q_DECLARE_PUBLIC(EwsFetchFoldersJob) +}; + +EwsFetchFoldersJobPrivate::EwsFetchFoldersJobPrivate(EwsFetchFoldersJob *parent, EwsClient &client, + const Collection &rootCollection) + : QObject(parent), mClient(client), mRootCollection(rootCollection), q_ptr(parent) +{ + mPendingFetchJobs = 0; + mPendingMoveJobs = 0; +} + +EwsFetchFoldersJobPrivate::~EwsFetchFoldersJobPrivate() +{ +} + +void EwsFetchFoldersJobPrivate::remoteFolderFullFetchDone(KJob *job) +{ + Q_Q(EwsFetchFoldersJob); + + EwsSyncFolderHierarchyRequest *req = qobject_cast(job); + if (!req) { + qCWarning(EWSRES_LOG) << QStringLiteral("Invalid EwsSyncFolderHierarchyRequest job object"); + q->setErrorMsg(QStringLiteral("Invalid EwsSyncFolderHierarchyRequest job object")); + q->emitResult(); + return; + } + + if (req->error()) { + /* It has been reported that the SyncFolderHierarchyRequest can fail with Internal Server + * Error (possibly because of a large number of folders). In order to work around this + * try to fallback to fetching just the folder identifiers and retrieve the details later. */ + qCDebug(EWSRES_LOG) << QStringLiteral("Full fetch failed. Trying to fetch ids only."); + + EwsSyncFolderHierarchyRequest *syncFoldersReq = new EwsSyncFolderHierarchyRequest(mClient, this); + syncFoldersReq->setFolderId(EwsId(EwsDIdMsgFolderRoot)); + EwsFolderShape shape(EwsShapeIdOnly); + syncFoldersReq->setFolderShape(shape); + connect(syncFoldersReq, &EwsSyncFolderHierarchyRequest::result, this, + &EwsFetchFoldersJobPrivate::remoteFolderIdFullFetchDone); + q->addSubjob(syncFoldersReq); + syncFoldersReq->start(); + + return; + } + + Q_FOREACH (const EwsSyncFolderHierarchyRequest::Change &ch, req->changes()) { + if (ch.type() == EwsSyncFolderHierarchyRequest::Create) { + mRemoteChangedFolders.append(ch.folder()); + } else { + q->setErrorMsg(QStringLiteral("Got non-create change for full sync.")); + q->emitResult(); + return; + } + } + + if (req->includesLastItem()) { + processRemoteFolders(); + + buildCollectionList(); + + q->mSyncState = req->syncState(); + + q->emitResult(); + } else { + EwsSyncFolderHierarchyRequest *syncFoldersReq = new EwsSyncFolderHierarchyRequest(mClient, this); + syncFoldersReq->setFolderId(EwsId(EwsDIdMsgFolderRoot)); + EwsFolderShape shape; + shape << propPidTagContainerClass; + shape << EwsPropertyField(QStringLiteral("folder:EffectiveRights")); + shape << EwsPropertyField(QStringLiteral("folder:ParentFolderId")); + syncFoldersReq->setFolderShape(shape); + syncFoldersReq->setSyncState(req->syncState()); + connect(syncFoldersReq, &EwsSyncFolderHierarchyRequest::result, this, + &EwsFetchFoldersJobPrivate::remoteFolderFullFetchDone); + syncFoldersReq->start(); + } +} + +void EwsFetchFoldersJobPrivate::remoteFolderIdFullFetchDone(KJob *job) +{ + Q_Q(EwsFetchFoldersJob); + + EwsSyncFolderHierarchyRequest *req = qobject_cast(job); + if (!req) { + qCWarning(EWSRES_LOG) << QStringLiteral("Invalid EwsSyncFolderHierarchyRequest job object"); + q->setErrorMsg(QStringLiteral("Invalid EwsSyncFolderHierarchyRequest job object")); + q->emitResult(); + return; + } + + if (req->error()) { + return; + } + + Q_FOREACH (const EwsSyncFolderHierarchyRequest::Change &ch, req->changes()) { + if (ch.type() == EwsSyncFolderHierarchyRequest::Create) { + mRemoteFolderIds.append(ch.folder()[EwsFolderFieldFolderId].value()); + } else { + q->setErrorMsg(QStringLiteral("Got non-create change for full sync.")); + q->emitResult(); + return; + } + } + + if (req->includesLastItem()) { + EwsFolderShape shape(EwsShapeDefault); + shape << propPidTagContainerClass; + shape << EwsPropertyField(QStringLiteral("folder:EffectiveRights")); + shape << EwsPropertyField(QStringLiteral("folder:ParentFolderId")); + mPendingFetchJobs = 0; + + for (int i = 0; i < mRemoteFolderIds.size(); i += fetchBatchSize) { + EwsGetFolderRequest *req = new EwsGetFolderRequest(mClient, this); + req->setFolderIds(mRemoteFolderIds.mid(i, fetchBatchSize)); + req->setFolderShape(shape); + connect(req, &EwsSyncFolderHierarchyRequest::result, this, + &EwsFetchFoldersJobPrivate::remoteFolderDetailFetchDone); + req->start(); + q->addSubjob(req); + mPendingFetchJobs++; + } + + qCDebugNC(EWSRES_LOG) << QStringLiteral("Starting %1 folder fetch jobs.").arg(mPendingFetchJobs); + + q->mSyncState = req->syncState(); + } else { + EwsSyncFolderHierarchyRequest *syncFoldersReq = new EwsSyncFolderHierarchyRequest(mClient, this); + syncFoldersReq->setFolderId(EwsId(EwsDIdMsgFolderRoot)); + EwsFolderShape shape(EwsShapeIdOnly); + syncFoldersReq->setFolderShape(shape); + syncFoldersReq->setSyncState(req->syncState()); + connect(syncFoldersReq, &EwsSyncFolderHierarchyRequest::result, this, + &EwsFetchFoldersJobPrivate::remoteFolderIdFullFetchDone); + q->addSubjob(syncFoldersReq); + syncFoldersReq->start(); + } +} + +void EwsFetchFoldersJobPrivate::remoteFolderDetailFetchDone(KJob *job) +{ + Q_Q(EwsFetchFoldersJob); + + EwsGetFolderRequest *req = qobject_cast(job); + if (!req) { + qCWarning(EWSRES_LOG) << QStringLiteral("Invalid EwsGetFolderRequest job object"); + q->setErrorMsg(QStringLiteral("Invalid EwsGetFolderRequest job object")); + q->emitResult(); + return; + } + + if (req->error()) { + return; + } + + Q_FOREACH (const EwsGetFolderRequest::Response &resp, req->responses()) { + if (resp.isSuccess()) { + mRemoteChangedFolders.append(resp.folder()); + } else { + qCWarningNC(EWSRES_LOG) << QStringLiteral("Failed to fetch folder details."); + } + } + + mPendingFetchJobs--; + qCDebugNC(EWSRES_LOG) << QStringLiteral("%1 folder fetch jobs pending").arg(mPendingFetchJobs); + + if (mPendingFetchJobs == 0) { + qCDebugNC(EWSRES_LOG) << QStringLiteral("All folder fetch jobs complete"); + + processRemoteFolders(); + + buildCollectionList(); + + + q->emitResult(); + } +} + +void EwsFetchFoldersJobPrivate::processRemoteFolders() +{ + /* mCollectionMap contains the global collection list keyed by the EWS ID. */ + /* mParentMap contains the parent->child map for each collection. */ + + /* Iterate over all changed folders. */ + Q_FOREACH (const EwsFolder &folder, mRemoteChangedFolders) { + /* Create a collection for each folder. */ + Collection c = createFolderCollection(folder); + + /* Insert it into the global collection list. */ + mCollectionMap.insert(c.remoteId(), c); + + /* Determine the parent and insert a parent->child relationship. + * Don't use Collection::setParentCollection() yet as the collection object will be updated + * which will cause the parent->child relationship to be broken. This happens because the + * collection object holds a copy of the parent collection object. An update to that + * object in the list will not be visible in the copy inside of the child object. */ + EwsId parentId = folder[EwsFolderFieldParentFolderId].value(); + mParentMap.insert(parentId.id(), c.remoteId()); + } +} + +void EwsFetchFoldersJobPrivate::buildCollectionList() +{ + Q_Q(EwsFetchFoldersJob); + + + q->mFolders.append(mRootCollection); + buildChildCollectionList(mRootCollection); + + if (!mCollectionMap.isEmpty()) { + q->setErrorMsg(QStringLiteral("Found orphaned collections")); + } +} + +void EwsFetchFoldersJobPrivate::buildChildCollectionList(const Collection &col) +{ + Q_Q(EwsFetchFoldersJob); + + QStringList children = mParentMap.values(col.remoteId()); + Q_FOREACH (const QString &childId, children) { + Collection child(mCollectionMap.take(childId)); + child.setParentCollection(col); + q->mFolders.append(child); + buildChildCollectionList(child); + } +} + +Collection EwsFetchFoldersJobPrivate::createFolderCollection(const EwsFolder &folder) +{ + Collection collection; + collection.setName(folder[EwsFolderFieldDisplayName].toString()); + QStringList mimeTypes; + QString contClass = folder[propPidTagContainerClass].toString(); + mimeTypes.append(Collection::mimeType()); + switch (folder.type()) { + case EwsFolderTypeCalendar: + mimeTypes.append(KCalCore::Event::eventMimeType()); + break; + case EwsFolderTypeContacts: + mimeTypes.append(KContacts::Addressee::mimeType()); + mimeTypes.append(KContacts::ContactGroup::mimeType()); + break; + case EwsFolderTypeTasks: + mimeTypes.append(KCalCore::Todo::todoMimeType()); + break; + case EwsFolderTypeMail: + if (contClass == QStringLiteral("IPF.Note") || contClass.isEmpty()) { + mimeTypes.append(KMime::Message::mimeType()); + } + break; + default: + break; + } + collection.setContentMimeTypes(mimeTypes); + Collection::Rights colRights; + EwsEffectiveRights ewsRights = folder[EwsFolderFieldEffectiveRights].value(); + // FIXME: For now full read/write support is only implemented for e-mail. In order to avoid + // potential problems block write access to all other folder types. + if (folder.type() == EwsFolderTypeMail) { + if (ewsRights.canDelete()) { + colRights |= Collection::CanDeleteCollection | Collection::CanDeleteItem; + } + if (ewsRights.canModify()) { + colRights |= Collection::CanChangeCollection | Collection::CanChangeItem; + } + if (ewsRights.canCreateContents()) { + colRights |= Collection::CanCreateItem; + } + if (ewsRights.canCreateHierarchy()) { + colRights |= Collection::CanCreateCollection; + } + } + collection.setRights(colRights); + EwsId id = folder[EwsFolderFieldFolderId].value(); + collection.setRemoteId(id.id()); + collection.setRemoteRevision(id.changeKey()); + + return collection; +} + +EwsFetchFoldersJob::EwsFetchFoldersJob(EwsClient &client, const Akonadi::Collection &rootCollection, + QObject *parent) + : EwsJob(parent), + d_ptr(new EwsFetchFoldersJobPrivate(this, client, rootCollection)) +{ + qRegisterMetaType(); +} + +EwsFetchFoldersJob::~EwsFetchFoldersJob() +{ +} + +void EwsFetchFoldersJob::start() +{ + Q_D(const EwsFetchFoldersJob); + + EwsSyncFolderHierarchyRequest *syncFoldersReq = new EwsSyncFolderHierarchyRequest(d->mClient, this); + syncFoldersReq->setFolderId(EwsId(EwsDIdMsgFolderRoot)); + EwsFolderShape shape; + shape << propPidTagContainerClass; + shape << EwsPropertyField(QStringLiteral("folder:EffectiveRights")); + shape << EwsPropertyField(QStringLiteral("folder:ParentFolderId")); + syncFoldersReq->setFolderShape(shape); + if (!mSyncState.isNull()) { + syncFoldersReq->setSyncState(mSyncState); + } + connect(syncFoldersReq, &EwsSyncFolderHierarchyRequest::result, d, + &EwsFetchFoldersJobPrivate::remoteFolderFullFetchDone); + // Don't add this as a subjob as the error is handled in its own way rather than throwing an + // error code to the parent. + + syncFoldersReq->start(); +} + diff --git a/resources/ews/ewsfetchitemdetailjob.h b/resources/ews/ewsfetchitemdetailjob.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsfetchitemdetailjob.h @@ -0,0 +1,64 @@ +/* + 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. +*/ + +#ifndef EWSFETCHITEMDETAILJOB_H +#define EWSFETCHITEMDETAILJOB_H + +#include + +#include + +#include +#include + +#include "ewsclient.h" +#include "ewsgetitemrequest.h" +#include "ewsid.h" +#include "ewsitem.h" +#include "ewstypes.h" + +class EwsFetchItemDetailJob : public KCompositeJob +{ + Q_OBJECT +public: + EwsFetchItemDetailJob(EwsClient &client, QObject *parent, const Akonadi::Collection &collection); + virtual ~EwsFetchItemDetailJob(); + + void setItemLists(Akonadi::Item::List changedItems, Akonadi::Item::List *deletedItems); + + Akonadi::Item::List changedItems() const + { + return mChangedItems; + } + + virtual void start() override; +protected: + virtual void processItems(const QList &responses) = 0; + + QPointer mRequest; + Akonadi::Item::List mChangedItems; + Akonadi::Item::List *mDeletedItems; + EwsClient &mClient; + const Akonadi::Collection mCollection; +private Q_SLOTS: + void itemDetailFetched(KJob *job); +private: +}; + +#endif diff --git a/resources/ews/ewsfetchitemdetailjob.cpp b/resources/ews/ewsfetchitemdetailjob.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsfetchitemdetailjob.cpp @@ -0,0 +1,64 @@ +/* + 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 "ewsfetchitemdetailjob.h" + +#include "ewsgetitemrequest.h" + +EwsFetchItemDetailJob::EwsFetchItemDetailJob(EwsClient &client, QObject *parent, const Akonadi::Collection &collection) + : KCompositeJob(parent), mDeletedItems(nullptr), mClient(client), mCollection(collection) +{ + mRequest = new EwsGetItemRequest(client, this); + connect(mRequest, SIGNAL(result(KJob*)), SLOT(itemDetailFetched(KJob*))); + addSubjob(mRequest); +} + +EwsFetchItemDetailJob::~EwsFetchItemDetailJob() +{ +} + +void EwsFetchItemDetailJob::setItemLists(Akonadi::Item::List changedItems, + Akonadi::Item::List *deletedItems) +{ + mChangedItems = changedItems; + mDeletedItems = deletedItems; + + EwsId::List ids; + + Q_FOREACH (const Akonadi::Item &item, changedItems) { + EwsId id(item.remoteId(), item.remoteRevision()); + ids.append(id); + } + + mRequest->setItemIds(ids); +} + +void EwsFetchItemDetailJob::itemDetailFetched(KJob *job) +{ + if (!job->error() && job == mRequest) { + Q_ASSERT(mChangedItems.size() == mRequest->responses().size()); + + processItems(mRequest->responses()); + } +} + +void EwsFetchItemDetailJob::start() +{ + mRequest->start(); +} diff --git a/resources/ews/ewsfetchitemsjob.h b/resources/ews/ewsfetchitemsjob.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsfetchitemsjob.h @@ -0,0 +1,112 @@ +/* + 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. +*/ + +#ifndef EWSFETCHITEMSJOB_H +#define EWSFETCHITEMSJOB_H + +#include + +#include "ewsjob.h" +#include "ewsfinditemrequest.h" + +namespace Akonadi +{ +class Collection; +} +class EwsClient; +class EwsTagStore; +class EwsResource; + +class EwsFetchItemsJob : public EwsJob +{ + Q_OBJECT +public: + struct QueuedUpdate { + QString id; + QString changeKey; + EwsEventType type; + }; + + typedef QList QueuedUpdateList; + + EwsFetchItemsJob(const Akonadi::Collection &collection, EwsClient &client, + const QString &syncState, const EwsId::List &itemsToCheck, + EwsTagStore *tagStore, EwsResource *parent); + virtual ~EwsFetchItemsJob(); + + Akonadi::Item::List changedItems() const + { + return mChangedItems; + }; + Akonadi::Item::List deletedItems() const + { + return mDeletedItems; + }; + const QString &syncState() const + { + return mSyncState; + }; + const Akonadi::Collection &collection() const + { + return mCollection; + }; + + void setQueuedUpdates(const QueuedUpdateList &updates); + + virtual void start() override; +private Q_SLOTS: + void localItemFetchDone(KJob *job); + void remoteItemFetchDone(KJob *job); + void itemDetailFetchDone(KJob *job); + void checkedItemsFetchFinished(KJob *job); + void tagSyncFinished(KJob *job); +Q_SIGNALS: + void status(int status, const QString &message = QString()); + void percent(int progress); +private: + void compareItemLists(); + void syncTags(); + + /*struct QueuedUpdateInt { + QString changeKey; + EwsEventType type; + };*/ + typedef QHash > QueuedUpdateHash; + + const Akonadi::Collection mCollection; + EwsClient &mClient; + EwsId::List mItemsToCheck; + Akonadi::Item::List mLocalItems; + EwsItem::List mRemoteAddedItems; + EwsItem::List mRemoteChangedItems; + EwsId::List mRemoteDeletedIds; + QHash mRemoteFlagChangedIds; + int mPendingJobs; + unsigned mTotalItems; + QString mSyncState; + bool mFullSync; + QueuedUpdateHash mQueuedUpdates; + EwsTagStore *mTagStore; + bool mTagsSynced; + + Akonadi::Item::List mChangedItems; + Akonadi::Item::List mDeletedItems; +}; + +#endif diff --git a/resources/ews/ewsfetchitemsjob.cpp b/resources/ews/ewsfetchitemsjob.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsfetchitemsjob.cpp @@ -0,0 +1,452 @@ +/* + 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 "ewsfetchitemsjob.h" + +#include +#include + +#include "ewsfinditemrequest.h" +#include "ewssyncfolderitemsrequest.h" +#include "ewsclient.h" +#include "ewsmailbox.h" +#include "ewsitemhandler.h" +#include "ewsfetchitemdetailjob.h" +#include "tags/ewstagstore.h" +#include "tags/ewsakonaditagssyncjob.h" +#include "ewsresource.h" +#include "ewsresource_debug.h" + +using namespace Akonadi; + +static Q_CONSTEXPR int listBatchSize = 100; +static Q_CONSTEXPR int fetchBatchSize = 50; + +/** + * The fetch items job is processed in two stages. + * + * The first stage is to query the list of messages on the remote and local sides. For this purpose + * an EwsSyncFolderItemsRequest is started to retrieve remote items (list of ids only) and an ItemFetchJob + * is started to fetch local items (from cache only). Both of these jobs are started simultaneously. + * + * The second stage begins when both item list query jobs have finished. The goal of this stage is + * to determine a list of items to fetch more details for. Since the EwsSyncFolderItemsRequest can + * retrieve incremental information further processing depends on whether the sync request was a + * full sync (no sync state) or an incremental sync. + * + * In case of a full sync both item lists are compared to determine lists of new/changed items and + * deleted items. For an incremental sync there is no need to compare as the lists of + * added/changed/deleted items are already given. 'IsRead' flag changes changes are treated + * specially - the modification is trivial and is performed straight away without fetching item + * details. + * + * The list of new/changed items is then used to perform a second remote request in order to + * retrieve the details of these items. For e-mail items the second fetch only retrieves the + * item headers. For other items the full MIME content is fetched. + * + * In case of an incremental sync the compare code checks if items marked as 'changed' or 'deleted' + * exist in Akonadi database. If not an error is raised. This serves as an information to the + * resource class that an incremental sync has failed due to an out-of-sync state and a full sync + * is required to bring thing back to order. + * + * In addition to a regular sync it is sometimes necessary to check for existence of some specific + * items. This happens when some operation failed and the resource tries to work its way around to + * get Akonadi into a synchronous state after the failure. For this purpose the caller can provide + * a list of item identifiers to look for besides the regular sync. If this list contains elements + * another request (EwsGetItemRequest) is issued in parallel for the selected items. Once this + * request returns the list is cross-checked with the server responses. If an item is found on the + * server side it is added to the "new items" list. Otherwise it is added to the "deleted items" + * list. In the latter case the code for determining if the deleted item is in the Akonadi database + * will not raise an error for such deleted item. This helps to avoid unnecessary full syncs. + * + * The fetch item job also allows providing a list of expected item changes. This is necessary + * because the incremental sync will return events for operations made by the resource itself. This + * can lead to false failures when for example an item was deleted (i.e. does not exist in Akonadi + * any more) and the incremental sync returns a delete event for this item. Typically this would + * result in an error and force a full sync. Providing this list allows for this particular error + * to be safely ignored. + */ + +EwsFetchItemsJob::EwsFetchItemsJob(const Collection &collection, EwsClient &client, + const QString &syncState, const EwsId::List &itemsToCheck, + EwsTagStore *tagStore, EwsResource *parent) + : EwsJob(parent), mCollection(collection), mClient(client), mItemsToCheck(itemsToCheck), + mPendingJobs(0), mTotalItems(0), mSyncState(syncState), mFullSync(syncState.isNull()), + mTagStore(tagStore), mTagsSynced(false) +{ + qRegisterMetaType(); +} + +EwsFetchItemsJob::~EwsFetchItemsJob() +{ +} + +void EwsFetchItemsJob::start() +{ + /* Begin stage 1 - query item list from local and remote side. */ + EwsSyncFolderItemsRequest *syncItemsReq = new EwsSyncFolderItemsRequest(mClient, this); + syncItemsReq->setFolderId(EwsId(mCollection.remoteId(), mCollection.remoteRevision())); + EwsItemShape shape(EwsShapeIdOnly); + shape << EwsResource::tagsProperty; + syncItemsReq->setItemShape(shape); + if (!mSyncState.isNull()) { + syncItemsReq->setSyncState(mSyncState); + } + syncItemsReq->setMaxChanges(listBatchSize); + connect(syncItemsReq, &EwsSyncFolderItemsRequest::result, this, &EwsFetchItemsJob::remoteItemFetchDone); + addSubjob(syncItemsReq); + + ItemFetchJob *itemJob = new ItemFetchJob(mCollection); + ItemFetchScope itemScope; + itemScope.setCacheOnly(true); + itemScope.fetchFullPayload(false); + itemJob->setFetchScope(itemScope); + connect(itemJob, &ItemFetchJob::result, this, &EwsFetchItemsJob::localItemFetchDone); + addSubjob(itemJob); + + mPendingJobs = 2; + syncItemsReq->start(); + itemJob->start(); + + if (!mItemsToCheck.isEmpty()) { + EwsGetItemRequest *getItemReq = new EwsGetItemRequest(mClient, this); + getItemReq->setItemIds(mItemsToCheck); + getItemReq->setItemShape(EwsItemShape(EwsShapeIdOnly)); + connect(getItemReq, &EwsGetItemRequest::result, this, &EwsFetchItemsJob::checkedItemsFetchFinished); + ++mPendingJobs; + getItemReq->start(); + } +} + +void EwsFetchItemsJob::localItemFetchDone(KJob *job) +{ + ItemFetchJob *fetchJob = qobject_cast(job); + + qCDebug(EWSRES_LOG) << "EwsFetchItemsJob::localItemFetchDone"; + if (!fetchJob) { + setErrorMsg(QStringLiteral("Invalid item fetch job pointer.")); + doKill(); + emitResult(); + return; + } + + if (!fetchJob->error()) { + removeSubjob(job); + mLocalItems = fetchJob->items(); + --mPendingJobs; + if (mPendingJobs == 0) { + compareItemLists(); + } + } +} + +void EwsFetchItemsJob::remoteItemFetchDone(KJob *job) +{ + EwsSyncFolderItemsRequest *itemReq = qobject_cast(job); + + qCDebug(EWSRES_LOG) << "EwsFetchItemsJob::remoteItemFetchDone"; + if (!itemReq) { + setErrorMsg(QStringLiteral("Invalid find item request pointer.")); + doKill(); + emitResult(); + return; + } + + if (!itemReq->error()) { + removeSubjob(job); + Q_FOREACH (const EwsSyncFolderItemsRequest::Change &change, itemReq->changes()) { + switch (change.type()) { + case EwsSyncFolderItemsRequest::Create: + mRemoteAddedItems.append(change.item()); + break; + case EwsSyncFolderItemsRequest::Update: + mRemoteChangedItems.append(change.item()); + break; + case EwsSyncFolderItemsRequest::Delete: + mRemoteDeletedIds.append(change.itemId()); + break; + case EwsSyncFolderItemsRequest::ReadFlagChange: + mRemoteFlagChangedIds.insert(change.itemId(), change.isRead()); + break; + default: + break; + } + } + + if (!itemReq->includesLastItem()) { + EwsSyncFolderItemsRequest *syncItemsReq = new EwsSyncFolderItemsRequest(mClient, this); + syncItemsReq->setFolderId(EwsId(mCollection.remoteId(), mCollection.remoteRevision())); + EwsItemShape shape(EwsShapeIdOnly); + shape << EwsResource::tagsProperty; + syncItemsReq->setItemShape(shape); + syncItemsReq->setSyncState(itemReq->syncState()); + syncItemsReq->setMaxChanges(listBatchSize); + connect(syncItemsReq, SIGNAL(result(KJob*)), SLOT(remoteItemFetchDone(KJob*))); + addSubjob(syncItemsReq); + syncItemsReq->start(); + } else { + mSyncState = itemReq->syncState(); + --mPendingJobs; + if (mPendingJobs == 0) { + compareItemLists(); + } + } + } +} + +void EwsFetchItemsJob::checkedItemsFetchFinished(KJob *job) +{ + EwsGetItemRequest *req = qobject_cast(job); + + if (!req) { + setErrorMsg(QStringLiteral("Invalid item fetch job pointer.")); + doKill(); + emitResult(); + return; + } + + if (!req->error()) { + removeSubjob(job); + + Q_ASSERT(mItemsToCheck.size() == req->responses().size()); + + EwsId::List::const_iterator it = mItemsToCheck.cbegin(); + Q_FOREACH (const EwsGetItemRequest::Response &resp, req->responses()) { + if (resp.isSuccess()) { + qCDebugNC(EWSRES_LOG) << QStringLiteral("Checked item %1 found - readding").arg(ewsHash(it->id())); + mRemoteAddedItems.append(resp.item()); + } else { + qCDebugNC(EWSRES_LOG) << QStringLiteral("Checked item %1 not found - removing").arg(ewsHash(it->id())); + mRemoteDeletedIds.append(*it); + } + ++it; + } + --mPendingJobs; + if (mPendingJobs == 0) { + compareItemLists(); + } + } +} + +void EwsFetchItemsJob::compareItemLists() +{ + /* Begin stage 2 - determine list of new/changed items and fetch details about them. */ + + Item::List toFetchItems[EwsItemTypeUnknown + 1]; + + Q_EMIT status(1, QStringLiteral("Retrieving items")); + Q_EMIT percent(0); + + QHash itemHash; + Q_FOREACH (const Item &item, mLocalItems) { + itemHash.insert(item.remoteId(), item); + } + + Q_FOREACH (const EwsItem &ewsItem, mRemoteAddedItems) { + /* In case of a full sync all existing items appear as added on the remote side. Therefore + * look for the item in the local list before creating a new copy. */ + EwsId id(ewsItem[EwsItemFieldItemId].value()); + QHash::iterator it = itemHash.find(id.id()); + EwsItemType type = ewsItem.internalType(); + if (type == EwsItemTypeUnknown) { + /* Ignore unknown items. */ + continue; + } + QString mimeType = EwsItemHandler::itemHandler(type)->mimeType(); + if (it == itemHash.end()) { + Item item(mimeType); + item.setParentCollection(mCollection); + EwsId id = ewsItem[EwsItemFieldItemId].value(); + item.setRemoteId(id.id()); + item.setRemoteRevision(id.changeKey()); + if (!mTagStore->readEwsProperties(item, ewsItem, mTagsSynced)) { + qCDebugNC(EWSRES_LOG) << QStringLiteral("Missing tags encountered - forcing sync"); + syncTags(); + return; + } + toFetchItems[type].append(item); + } else { + Item &item = *it; + item.clearPayload(); + item.setRemoteRevision(id.changeKey()); + if (!mTagStore->readEwsProperties(item, ewsItem, mTagsSynced)) { + qCDebugNC(EWSRES_LOG) << QStringLiteral("Missing tags encountered - forcing sync"); + syncTags(); + return; + } + toFetchItems[type].append(item); + itemHash.erase(it); + } + } + + if (mFullSync) { + /* In case of a full sync all items that are still on the local item list do not exist + * remotely and need to be deleted locally. */ + QHash::iterator it; + for (it = itemHash.begin(); it != itemHash.end(); ++it) { + mDeletedItems.append(it.value()); + } + } else { + Q_FOREACH (const EwsItem &ewsItem, mRemoteChangedItems) { + EwsId id(ewsItem[EwsItemFieldItemId].value()); + QHash::iterator it = itemHash.find(id.id()); + if (it == itemHash.end()) { + setErrorMsg(QStringLiteral("Got update for item %1, but item not found in local store.") + .arg(ewsHash(id.id()))); + emitResult(); + return; + } + Item &item = *it; + item.clearPayload(); + item.setRemoteRevision(id.changeKey()); + if (!mTagStore->readEwsProperties(item, ewsItem, mTagsSynced)) { + qCDebugNC(EWSRES_LOG) << QStringLiteral("Missing tags encountered - forcing sync"); + syncTags(); + return; + } + EwsItemType type = ewsItem.internalType(); + toFetchItems[type].append(item); + itemHash.erase(it); + } + + // In case of an incremental sync deleted items will be given explicitly. */ + Q_FOREACH (const EwsId &id, mRemoteDeletedIds) { + QHash::iterator it = itemHash.find(id.id()); + /* If one or more items marked as deleted are not found it means that the folder is out + * of sync. The only way to fix this is to issue a full sync. + * The only exception is when an item is checked explicitly. In such case the absence + * of this item can be ignored. */ + if (it == itemHash.end()) { + QHash::iterator qit = mQueuedUpdates[EwsDeletedEvent].find(id.id()); + if (EWSRES_LOG().isDebugEnabled() && qit != mQueuedUpdates[EwsDeletedEvent].end()) { + qCDebugNC(EWSRES_LOG) << QStringLiteral("Match for queued deletion of item %1").arg(ewsHash(id.id())); + } + if (!mItemsToCheck.contains(id) && qit == mQueuedUpdates[EwsDeletedEvent].end()) { + setErrorMsg(QStringLiteral("Got delete for item %1, but item not found in local store.") + .arg(ewsHash(id.id()))); + emitResult(); + return; + } + } else { + mDeletedItems.append(*it); + } + } + + QHash::const_iterator it; + EwsItemHandler *handler = EwsItemHandler::itemHandler(EwsItemTypeMessage); + for (it = mRemoteFlagChangedIds.cbegin(); it != mRemoteFlagChangedIds.cend(); ++it) { + QHash::iterator iit = itemHash.find(it.key().id()); + if (iit == itemHash.end()) { + setErrorMsg(QStringLiteral("Got read flag change for item %1, but item not found in local store.") + .arg(it.key().id())); + emitResult(); + return; + } + Item &item = *iit; + handler->setSeenFlag(item, it.value()); + mChangedItems.append(item); + itemHash.erase(iit); + } + } + + qCDebugNC(EWSRES_LOG) << QStringLiteral("Changed %2, deleted %3, new %4") + .arg(mRemoteChangedItems.size()) + .arg(mDeletedItems.size()).arg(mRemoteAddedItems.size()); + + bool fetch = false; + for (unsigned iType = 0; iType < sizeof(toFetchItems) / sizeof(toFetchItems[0]); ++iType) { + if (!toFetchItems[iType].isEmpty()) { + for (int i = 0; i < toFetchItems[iType].size(); i += fetchBatchSize) { + EwsItemHandler *handler = EwsItemHandler::itemHandler(static_cast(iType)); + if (!handler) { + // TODO: Temporarily ignore unsupported item types. + qCWarning(EWSRES_LOG) << QStringLiteral("Unable to initialize fetch for item type %1") + .arg(iType); + /*setErrorMsg(QStringLiteral("Unable to initialize fetch for item type %1").arg(iType)); + emitResult(); + return;*/ + } else { + EwsFetchItemDetailJob *job = handler->fetchItemDetailJob(mClient, this, mCollection); + Item::List itemList = toFetchItems[iType].mid(i, fetchBatchSize); + job->setItemLists(itemList, &mDeletedItems); + connect(job, SIGNAL(result(KJob*)), SLOT(itemDetailFetchDone(KJob*))); + addSubjob(job); + fetch = true; + } + } + } + } + if (!fetch) { + // Nothing to fetch - we're done here. + emitResult(); + } else { + subjobs().first()->start(); + } +} + +void EwsFetchItemsJob::itemDetailFetchDone(KJob *job) +{ + removeSubjob(job); + + if (!job->error()) { + EwsFetchItemDetailJob *detailJob = qobject_cast(job); + if (detailJob) { + mChangedItems += detailJob->changedItems(); + } + + if (subjobs().size() == 0) { + emitResult(); + } else { + subjobs().first()->start(); + } + } +} + +void EwsFetchItemsJob::setQueuedUpdates(const QueuedUpdateList &updates) +{ + mQueuedUpdates.clear(); + Q_FOREACH (const QueuedUpdate &upd, updates) { + mQueuedUpdates[upd.type].insert(upd.id, upd.changeKey); + qCDebugNC(EWSRES_LOG) << QStringLiteral("Queued update %1 for item %2").arg(upd.type).arg(ewsHash(upd.id)); + } +} + +void EwsFetchItemsJob::syncTags() +{ + if (mTagsSynced) { + setErrorMsg(QStringLiteral("Missing tags encountered despite previous sync.")); + emitResult(); + } else { + EwsAkonadiTagsSyncJob *job = new EwsAkonadiTagsSyncJob(mTagStore, mClient, + qobject_cast(parent())->rootCollection(), this); + connect(job, &EwsAkonadiTagsSyncJob::result, this, &EwsFetchItemsJob::tagSyncFinished); + job->start(); + mTagsSynced = true; + } +} + +void EwsFetchItemsJob::tagSyncFinished(KJob *job) +{ + if (job->error()) { + setErrorMsg(job->errorText()); + emitResult(); + } else { + compareItemLists(); + } +} diff --git a/resources/ews/ewsitemhandler.h b/resources/ews/ewsitemhandler.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsitemhandler.h @@ -0,0 +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 EWSITEMHANDLER_H +#define EWSITEMHANDLER_H + +#include + +#include + +#include "ewspropertyfield.h" +#include "ewstypes.h" + +namespace Akonadi +{ +class Collection; +class Item; +} +class EwsClient; +class EwsFetchItemDetailJob; +class EwsModifyItemJob; +class EwsCreateItemJob; +class EwsItem; +class EwsTagStore; +class EwsResource; + +class EwsItemHandler +{ +public: + virtual ~EwsItemHandler(); + + virtual EwsFetchItemDetailJob *fetchItemDetailJob(EwsClient &client, QObject *parent, + const Akonadi::Collection &collection) = 0; + virtual void setSeenFlag(Akonadi::Item &item, bool value) = 0; + virtual QString mimeType() = 0; + virtual bool setItemPayload(Akonadi::Item &item, const EwsItem &ewsItem) = 0; + virtual EwsModifyItemJob *modifyItemJob(EwsClient &client, const QVector &items, + const QSet &parts, QObject *parent) = 0; + virtual EwsCreateItemJob *createItemJob(EwsClient &client, const Akonadi::Item &item, + const Akonadi::Collection &collection, + EwsTagStore *tagStore, EwsResource *parent) = 0; + + typedef std::function ItemHandlerFactory; + static void registerItemHandler(EwsItemType type, ItemHandlerFactory factory); + static EwsItemHandler *itemHandler(EwsItemType type); + static EwsItemType mimeToItemType(const QString &mimeType); + static QHash writeFlags(const QSet &flags); + static QSet readFlags(const EwsItem &item); + static QList flagsProperties(); + static QList tagsProperties(); +private: + struct HandlerFactory { + EwsItemType type; + ItemHandlerFactory factory; + }; +}; + +#define EWS_DECLARE_ITEM_HANDLER(clsname, type) \ + class type ## _itemhandler_registrar { \ + public: \ + type ## _itemhandler_registrar() \ + { \ + EwsItemHandler::registerItemHandler(type, &clsname::factory); \ + } \ + }; \ + const type ## _itemhandler_registrar type ## _itemhandler_registrar_object; + +#endif diff --git a/resources/ews/ewsitemhandler.cpp b/resources/ews/ewsitemhandler.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsitemhandler.cpp @@ -0,0 +1,120 @@ +/* + 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 "ewsitemhandler.h" + +#include "ewsresource.h" +#include "ewsresource_debug.h" + +struct HandlerFactory { + EwsItemType type; + EwsItemHandler::ItemHandlerFactory factory; +}; + +typedef QList HandlerList; +typedef QHash> HandlerHash; + +Q_GLOBAL_STATIC(HandlerList, handlerFactories) +Q_GLOBAL_STATIC(HandlerHash, handlers) + +EwsItemHandler::~EwsItemHandler() +{ +} + +void EwsItemHandler::registerItemHandler(EwsItemType type, ItemHandlerFactory factory) +{ + handlerFactories->append({type, factory}); +} +EwsItemHandler *EwsItemHandler::itemHandler(EwsItemType type) +{ + HandlerHash::iterator it = handlers->find(type); + if (it != handlers->end()) { + return it->data(); + } else { + HandlerList::const_iterator it; + for (it = handlerFactories->cbegin(); it != handlerFactories->cend(); ++it) { + if (it->type == type) { + EwsItemHandler *handler = it->factory(); + (*handlers)[type].reset(handler); + return handler; + } + } + qCWarning(EWSRES_LOG) << QStringLiteral("Could not find handler for item type %1").arg(type); + + return nullptr; + } +} + +EwsItemType EwsItemHandler::mimeToItemType(const QString &mimeType) +{ + if (mimeType == itemHandler(EwsItemTypeMessage)->mimeType()) { + return EwsItemTypeMessage; + } else if (mimeType == itemHandler(EwsItemTypeCalendarItem)->mimeType()) { + return EwsItemTypeCalendarItem; + } else if (mimeType == itemHandler(EwsItemTypeTask)->mimeType()) { + return EwsItemTypeTask; + } else if (mimeType == itemHandler(EwsItemTypeContact)->mimeType()) { + return EwsItemTypeContact; + } else { + return EwsItemTypeItem; + } +} + +QHash EwsItemHandler::writeFlags(const QSet &flags) +{ + QHash propertyHash; + + if (flags.isEmpty()) { + propertyHash.insert(EwsResource::flagsProperty, QVariant()); + } else { + QStringList flagList; + Q_FOREACH (const QByteArray &flag, flags) { + flagList.append(QString::fromLatin1(flag)); + } + propertyHash.insert(EwsResource::flagsProperty, flagList); + } + + return propertyHash; +} + +QSet EwsItemHandler::readFlags(const EwsItem &item) +{ + QSet flags; + + QVariant flagProp = item[EwsResource::flagsProperty]; + if (!flagProp.isNull() && (flagProp.canConvert())) { + QStringList flagList = flagProp.toStringList(); + Q_FOREACH (const QString &flag, flagList) { + flags.insert(flag.toAscii()); + } + } + + return flags; +} + +QList EwsItemHandler::flagsProperties() +{ + return {EwsResource::flagsProperty}; +} + +QList EwsItemHandler::tagsProperties() +{ + return {EwsResource::tagsProperty}; +} + diff --git a/resources/ews/ewsmodifyitemflagsjob.h b/resources/ews/ewsmodifyitemflagsjob.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsmodifyitemflagsjob.h @@ -0,0 +1,55 @@ +/* + 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. +*/ + +#ifndef EWSMODIFYITEMFLAGSJOB_H +#define EWSMODIFYITEMFLAGSJOB_H + +#include +#include + +#include "ewstypes.h" +#include "ewsclient.h" +#include "ewsjob.h" + +class EwsModifyItemFlagsJob : public EwsJob +{ + Q_OBJECT +public: + EwsModifyItemFlagsJob(EwsClient &client, QObject *parent, const Akonadi::Item::List, + const QSet &addedFlags, const QSet &removedFlags); + virtual ~EwsModifyItemFlagsJob(); + + Akonadi::Item::List items() const + { + return mResultItems; + } + + virtual void start() override; +protected: + Akonadi::Item::List mItems; + Akonadi::Item::List mResultItems; + EwsClient &mClient; + QSet mAddedFlags; + QSet mRemovedFlags; +private Q_SLOTS: + void itemModifyFinished(KJob *job); +private: +}; + +#endif diff --git a/resources/ews/ewsmodifyitemflagsjob.cpp b/resources/ews/ewsmodifyitemflagsjob.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsmodifyitemflagsjob.cpp @@ -0,0 +1,93 @@ +/* + Copyright (C) 2015-2016 Krzysztof Nowicki + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "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())); + 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/ewsmodifyitemjob.h b/resources/ews/ewsmodifyitemjob.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsmodifyitemjob.h @@ -0,0 +1,48 @@ +/* + 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. +*/ + +#ifndef EWSMODIFYITEMJOB_H +#define EWSMODIFYITEMJOB_H + +#include +#include "ewsjob.h" + +class EwsClient; + +class EwsModifyItemJob : public EwsJob +{ + Q_OBJECT +public: + EwsModifyItemJob(EwsClient &client, const Akonadi::Item::List &items, const QSet &parts, + QObject *parent); + virtual ~EwsModifyItemJob(); + + void setModifiedFlags(const QSet &addedFlags, const QSet &removedFlags); + + const Akonadi::Item::List &items() const; + +protected: + Akonadi::Item::List mItems; + const QSet mParts; + EwsClient &mClient; + QSet mAddedFlags; + QSet mRemovedFlags; +}; + +#endif diff --git a/resources/ews/ewsmodifyitemjob.cpp b/resources/ews/ewsmodifyitemjob.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsmodifyitemjob.cpp @@ -0,0 +1,42 @@ +/* + 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 "ewsmodifyitemjob.h" + +EwsModifyItemJob::EwsModifyItemJob(EwsClient &client, const Akonadi::Item::List &items, + const QSet &parts, QObject *parent) + : EwsJob(parent), mItems(items), mParts(parts), mClient(client) +{ +} + +EwsModifyItemJob::~EwsModifyItemJob() +{ +} + +void EwsModifyItemJob::setModifiedFlags(const QSet &addedFlags, + const QSet &removedFlags) +{ + mAddedFlags = addedFlags; + mRemovedFlags = removedFlags; +} + +const Akonadi::Item::List &EwsModifyItemJob::items() const +{ + return mItems; +} diff --git a/resources/ews/ewsmtaresource.h b/resources/ews/ewsmtaresource.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsmtaresource.h @@ -0,0 +1,62 @@ +/* + 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. +*/ + +#ifndef EWSMTARESOURCE_H +#define EWSMTARESOURCE_H + +#include +#include +#include +#include + +// Some older variants of akonadi_version.h use a different name +#ifndef AKONADI_VERSION +#define AKONADI_VERSION AKONADILIBRARIES_VERSION +#endif + +class OrgKdeAkonadiEwsResourceInterface; + +class EwsMtaResource : public Akonadi::ResourceBase, public Akonadi::TransportResourceBase +{ + Q_OBJECT +public: + explicit EwsMtaResource(const QString &id); + ~EwsMtaResource(); + + virtual void sendItem(const Akonadi::Item &item) override; +public Q_SLOTS: + void configure(WId windowId) override; +protected Q_SLOTS: + void retrieveCollections() override; + void retrieveItems(const Akonadi::Collection &collection) override; +#if (AKONADI_VERSION > 0x50328) + bool retrieveItems(const Akonadi::Item::List &items, const QSet &parts) override; +#else + bool retrieveItem(const Akonadi::Item &item, const QSet &parts) override; +#endif +private Q_SLOTS: + void messageSent(const QString &id, const QString &error); +private: + bool connectEws(); + + OrgKdeAkonadiEwsResourceInterface *mEwsResource; + QHash mItemHash; +}; + +#endif diff --git a/resources/ews/ewsmtaresource.cpp b/resources/ews/ewsmtaresource.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsmtaresource.cpp @@ -0,0 +1,145 @@ +/* + 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 "ewsmtaresource.h" + +#include +#include + +#include +#include + +#include "ewsresource_debug.h" +#include "mtaconfigdialog.h" +#include "mtasettings.h" +#include "resourceinterface.h" + +using namespace Akonadi; + +EwsMtaResource::EwsMtaResource(const QString &id) + : Akonadi::ResourceBase(id), mEwsResource(nullptr) +{ +} + +EwsMtaResource::~EwsMtaResource() +{ +} + +bool EwsMtaResource::connectEws() +{ + if (mEwsResource) { + return true; + } + mEwsResource = new OrgKdeAkonadiEwsResourceInterface( + QStringLiteral("org.freedesktop.Akonadi.Resource.") + MtaSettings::ewsResource(), + QStringLiteral("/"), QDBusConnection::sessionBus(), this); + if (!mEwsResource->isValid()) { + delete mEwsResource; + mEwsResource = nullptr; + } else { + connect(mEwsResource, &OrgKdeAkonadiEwsResourceInterface::messageSent, this, &EwsMtaResource::messageSent); + } + + return mEwsResource != nullptr; +} + +void EwsMtaResource::configure(WId windowId) +{ + QPointer dlg = new MtaConfigDialog(this, windowId); + if (dlg->exec()) { + MtaSettings::self()->save(); + } + delete dlg; +} + +void EwsMtaResource::sendItem(const Akonadi::Item &item) +{ + if (!connectEws()) { + itemSent(item, TransportFailed, i18n("Unable to connect to master EWS resource")); + return; + } + + mItemHash.insert(item.remoteId(), item); + + KMime::Message::Ptr msg = item.payload(); + /* Exchange doesn't just store whatever MIME content that was sent to it - it will parse it and send + * further the version assembled back from the parsed parts. It seems that this parsing doesn't work well + * with the quoted-printable encoding, which KMail prefers. This results in malformed encoding, which the + * sender doesn't even see. + * As a workaround force encoding of the body (or in case of multipart - all parts) to Base64. */ + if (msg->contents().isEmpty()) { + msg->changeEncoding(KMime::Headers::CEbase64); + msg->contentTransferEncoding(true)->setEncoding(KMime::Headers::CEbase64); + } else { + Q_FOREACH (KMime::Content *content, msg->contents()) { + content->changeEncoding(KMime::Headers::CEbase64); + content->contentTransferEncoding(true)->setEncoding(KMime::Headers::CEbase64); + } + } + msg->assemble(); + QByteArray mimeContent = msg->encodedContent(true); + mEwsResource->sendMessage(item.remoteId(), mimeContent); +} + +void EwsMtaResource::messageSent(const QString &id, const QString &error) +{ + QHash::iterator it = mItemHash.find(id); + if (it != mItemHash.end()) { + if (error.isEmpty()) { + itemSent(*it, TransportSucceeded, QString()); + } else { + itemSent(*it, TransportFailed, error); + } + mItemHash.erase(it); + } +} + +void EwsMtaResource::retrieveCollections() +{ + collectionsRetrieved(Collection::List()); +} + +void EwsMtaResource::retrieveItems(const Akonadi::Collection &collection) +{ + Q_UNUSED(collection) + + itemsRetrieved(Item::List()); +} + +#if (AKONADI_VERSION > 0x50328) +bool EwsMtaResource::retrieveItems(const Akonadi::Item::List &items, const QSet &parts) +{ + Q_UNUSED(parts) + + itemsRetrieved(items); + + return true; +} +#else +bool EwsMtaResource::retrieveItem(const Akonadi::Item &item, const QSet &parts) +{ + Q_UNUSED(parts) + + itemRetrieved(item); + + return true; +} +#endif + +AKONADI_RESOURCE_MAIN(EwsMtaResource) diff --git a/resources/ews/ewsmtaresource.desktop b/resources/ews/ewsmtaresource.desktop new file mode 100644 --- /dev/null +++ b/resources/ews/ewsmtaresource.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Name=Microsoft Exchange Server (EWS) Mail Transport +Comment="Allows sending e-mail through the Microsoft Exchange server using EWS" +Type=AkonadiResource +Exec=akonadi_ewsmta_resource +Icon=akonadi-ews + +X-Akonadi-MimeTypes=text/directory,message/rfc822 +X-Akonadi-Capabilities=NeedsNetwork,MailTransport +X-Akonadi-Identifier=akonadi_ewsmta_resource diff --git a/resources/ews/ewsmtaresource.kcfg b/resources/ews/ewsmtaresource.kcfg new file mode 100644 --- /dev/null +++ b/resources/ews/ewsmtaresource.kcfg @@ -0,0 +1,15 @@ + + + + + + + The name of the parent EWS Resource to use when sending mail + The name of the parent EWS Resource to use when sending mail + + + diff --git a/resources/ews/ewsresource.h b/resources/ews/ewsresource.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsresource.h @@ -0,0 +1,179 @@ +/* + 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 EWSRESOURCE_H +#define EWSRESOURCE_H + +#include + +#include +#include +#include + +#include "ewsclient.h" +#include "ewsfetchitemsjob.h" +#include "ewsid.h" + +#include + +// Some older variants of akonadi_version.h use a different name +#ifndef AKONADI_VERSION +#define AKONADI_VERSION AKONADILIBRARIES_VERSION +#endif + +class FetchItemState; +class EwsGetItemRequest; +class EwsFindFolderRequest; +class EwsFolder; +class EwsSubscriptionManager; +class EwsTagStore; +class Settings; + +class EwsResource : public Akonadi::ResourceBase, public Akonadi::AgentBase::ObserverV4, + public Akonadi::TransportResourceBase +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.Akonadi.Ews.Resource") +public: + static const QString akonadiEwsPropsetUuid; + static const EwsPropertyField globalTagsProperty; + static const EwsPropertyField globalTagsVersionProperty; + static const EwsPropertyField tagsProperty; + static const EwsPropertyField flagsProperty; + + explicit EwsResource(const QString &id); + ~EwsResource(); + + virtual void itemsTagsChanged(const Akonadi::Item::List &items, const QSet &addedTags, + const QSet &removedTags) override; + virtual void tagAdded(const Akonadi::Tag &tag) override; + virtual void tagChanged(const Akonadi::Tag &tag) override; + virtual void tagRemoved(const Akonadi::Tag &tag) override; + + virtual void collectionAdded(const Akonadi::Collection &collection, const Akonadi::Collection &parent) override; + virtual void collectionMoved(const Akonadi::Collection &collection, const Akonadi::Collection &collectionSource, + const Akonadi::Collection &collectionDestination) override; + virtual void collectionChanged(const Akonadi::Collection &collection, + const QSet &changedAttributes) override; + virtual void collectionChanged(const Akonadi::Collection &collection) override; + virtual void collectionRemoved(const Akonadi::Collection &collection) override; + virtual void itemAdded(const Akonadi::Item &item, const Akonadi::Collection &collection) override; + virtual void itemChanged(const Akonadi::Item &item, const QSet &partIdentifiers) override; + virtual void itemsFlagsChanged(const Akonadi::Item::List &items, const QSet &addedFlags, + const QSet &removedFlags) override; + virtual void itemsMoved(const Akonadi::Item::List &items, const Akonadi::Collection &sourceCollection, + const Akonadi::Collection &destinationCollection) override; + virtual void itemsRemoved(const Akonadi::Item::List &items) override; + + virtual void sendItem(const Akonadi::Item &item) override; + + const Akonadi::Collection &rootCollection() const + { + return mRootCollection; + }; + + Settings *settings() + { + return mSettings.data(); + }; +protected: + void doSetOnline(bool online) override; +public Q_SLOTS: + void configure(WId windowId) override; + Q_SCRIPTABLE void clearFolderSyncState(const QString &folderId); + Q_SCRIPTABLE void clearFolderSyncState(); + Q_SCRIPTABLE void clearFolderTreeSyncState(); +protected Q_SLOTS: + void retrieveCollections() override; + void retrieveItems(const Akonadi::Collection &collection) override; +#if (AKONADI_VERSION > 0x50328) + bool retrieveItems(const Akonadi::Item::List &items, const QSet &parts) override; +#else + bool retrieveItem(const Akonadi::Item &item, const QSet &parts) override; +#endif + void retrieveTags() override; +private Q_SLOTS: + void fetchFoldersJobFinished(KJob *job); + void fetchFoldersIncrJobFinished(KJob *job); + void itemFetchJobFinished(KJob *job); +#if (AKONADI_VERSION > 0x50328) + void getItemsRequestFinished(KJob *job); +#else + void getItemRequestFinished(KJob *job); +#endif + void itemChangeRequestFinished(KJob *job); + void itemModifyFlagsRequestFinished(KJob *job); + void itemMoveRequestFinished(KJob *job); + void itemDeleteRequestFinished(KJob *job); + void itemCreateRequestFinished(KJob *job); + void itemSendRequestFinished(KJob *job); + void folderCreateRequestFinished(KJob *job); + void folderMoveRequestFinished(KJob *job); + void folderUpdateRequestFinished(KJob *job); + void folderDeleteRequestFinished(KJob *job); + void delayedInit(); + void foldersModifiedEvent(EwsId::List folders); + void foldersModifiedCollectionSyncFinished(KJob *job); + void folderTreeModifiedEvent(); + void fullSyncRequestedEvent(); + void rootFolderFetchFinished(KJob *job); + void specialFoldersFetchFinished(KJob *job); + void itemsTagChangeFinished(KJob *job); + void globalTagChangeFinished(KJob *job); + void globalTagsRetrievalFinished(KJob *job); + void adjustInboxRemoteIdFetchFinished(KJob *job); + void rootCollectionFetched(KJob *job); + void connectionError(); + void reloadConfig(); +public Q_SLOTS: + Q_SCRIPTABLE void sendMessage(const QString &id, const QByteArray &content); +Q_SIGNALS: + Q_SCRIPTABLE void messageSent(const QString &id, const QString &error); +#ifdef HAVE_SEPARATE_MTA_RESOURCE +private Q_SLOTS: + void messageSendRequestFinished(KJob *job); +#endif + +private: + void finishItemsFetch(FetchItemState *state); + void fetchSpecialFolders(); + void specialFoldersCollectionsRetrieved(const Akonadi::Collection::List &folders); + + void saveState(); + void resetUrl(); + + void doRetrieveCollections(); + + int reconnectTimeout(); + + EwsClient mEwsClient; + Akonadi::Collection mRootCollection; + QScopedPointer mSubManager; + QHash mSyncState; + QString mFolderSyncState; + QHash mItemsToCheck; + QHash mQueuedUpdates; + QString mPassword; + bool mTagsRetrieved; + int mReconnectTimeout; + EwsTagStore *mTagStore; + QScopedPointer mSettings; +}; + +#endif diff --git a/resources/ews/ewsresource.cpp b/resources/ews/ewsresource.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewsresource.cpp @@ -0,0 +1,1400 @@ +/* + 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 "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 "configdialog.h" +#include "settings.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 "resourceadaptor.h" +#include "settingsadaptor.h" +#include "walletadaptor.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), mTagsRetrieved(false), mReconnectTimeout(InitialReconnectTimeout), + mSettings(new Settings(winIdForDialogs())) +{ + //setName(i18n("Microsoft Exchange")); + mEwsClient.setUrl(mSettings->baseUrl()); + mSettings->requestPassword(mPassword, true); + if (!mSettings->hasDomain()) { + mEwsClient.setCredentials(mSettings->username(), mPassword); + } else { + mEwsClient.setCredentials(mSettings->domain() + QChar::fromLatin1('\\') + mSettings->username(), mPassword); + } + 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); + + if (mSettings->baseUrl().isEmpty()) { + setOnline(false); + Q_EMIT status(NotConfigured, i18nc("@info:status", "No server configured yet.")); + } else { + resetUrl(); + } + + // Load the sync state + QByteArray data = QByteArray::fromBase64(mSettings->syncState().toAscii()); + if (!data.isEmpty()) { + data = qUncompress(data); + if (!data.isEmpty()) { + QDataStream stream(data); + stream >> mSyncState; + } + } + data = QByteArray::fromBase64(mSettings->folderSyncState().toAscii()); + if (!data.isEmpty()) { + data = qUncompress(data); + if (!data.isEmpty()) { + mFolderSyncState = QString::fromAscii(data); + } + } + + setHierarchicalRemoteIdentifiersEnabled(true); + + mTagStore = new EwsTagStore(this); + + QMetaObject::invokeMethod(this, "delayedInit", Qt::QueuedConnection); + + connect(this, &AgentBase::reloadConfiguration, this, &EwsResource::reloadConfig); +} + +EwsResource::~EwsResource() +{ +} + +void EwsResource::delayedInit() +{ + new ResourceAdaptor(this); + new SettingsAdaptor(mSettings.data()); + new WalletAdaptor(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); + EwsId::List folders; + folders << EwsId(EwsDIdMsgFolderRoot); + folders << 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) { +#ifdef HAVE_INBOX_FILTERING_WORKAROUND + EwsId::setInboxId(id); + Collection c; + c.setRemoteId(id.id()); + CollectionFetchJob *job = new CollectionFetchJob(c, CollectionFetchJob::Base, this); + job->setFetchScope(changeRecorder()->collectionFetchScope()); + job->fetchScope().setResource(identifier()); + job->fetchScope().setListFilter(CollectionFetchScope::Sync); + connect(job, &CollectionFetchJob::result, this, &EwsResource::adjustInboxRemoteIdFetchFinished); +#else + 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(QStringLiteral("INBOX")); + if (inboxIdx >= 0) { + QStringList subList = mSettings->serverSubscriptionList(); + subList[inboxIdx] = id.id(); + mSettings->setServerSubscriptionList(subList); + } +#endif + } + + 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; + Q_EMIT status(Idle, i18nc("@info:status Resource is ready", "Ready")); + + 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]; +#ifdef HAVE_INBOX_FILTERING_WORKAROUND + c.setRemoteId(QStringLiteral("INBOX")); +#else + c.setRemoteId(fetchJob->property("inboxId").toString()); +#endif + 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) +{ + Q_EMIT status(1, QStringLiteral("Retrieving item list")); + + Q_EMIT status(Running, i18nc("@info:status", "Retrieving %1 items", collection.name())); + + 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) { + status(s, message); + }); + connect(job, &EwsFetchItemsJob::percent, this, [this](int p) { + percent(p); + }); + job->start(); +} + +#if (AKONADI_VERSION > 0x50328) +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; + Q_FOREACH (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; + } + + Item::List items = req->property("items").value(); + + QHash itemHash; + Q_FOREACH (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()); +} +#else +bool EwsResource::retrieveItem(const Item &item, const QSet &parts) +{ + qCDebugNC(EWSRES_AGENTIF_LOG) << "retrieveItem: start " << item << parts; + + EwsGetItemRequest *req = new EwsGetItemRequest(mEwsClient, this); + EwsId::List ids; + ids << EwsId(item.remoteId(), item.remoteRevision()); + req->setItemIds(ids); + EwsItemShape shape(EwsShapeIdOnly); + shape << EwsPropertyField("item:MimeContent"); + req->setItemShape(shape); + req->setProperty("item", QVariant::fromValue(item)); + connect(req, &EwsGetItemRequest::result, this, &EwsResource::getItemRequestFinished); + req->start(); + return true; +} + +void EwsResource::getItemRequestFinished(KJob *job) +{ + if (job->error()) { + qWarning() << "ERROR" << job->errorString(); + cancelTask(i18nc("@info:status", "Failed to process item 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 item - internal error")); + return; + } + + Item item = req->property("item").value(); + const EwsGetItemRequest::Response &resp = req->responses()[0]; + if (!resp.isSuccess()) { + qCWarningNC(EWSRES_AGENTIF_LOG) << QStringLiteral("retrieveItem: Item fetch failed!"); + cancelTask(i18nc("@info:status", "Failed to retrieve item")); + return; + } + const EwsItem &ewsItem = resp.item(); + EwsItemType type = ewsItem.internalType(); + if (type == EwsItemTypeUnknown) { + qCWarningNC(EWSRES_AGENTIF_LOG) << QStringLiteral("retrieveItem: Unknown item type!"); + cancelTask(i18nc("@info:status", "Failed to retrieve item - Unknown item type %1", id.id())); + return; + } + if (!EwsItemHandler::itemHandler(type)->setItemPayload(item, ewsItem)) { + qCWarningNC(EWSRES_AGENTIF_LOG) << QStringLiteral("retrieveItem: Failed to fetch item payload."); + cancelTask(i18nc("@info:status", "Failed to fetch item payload")); + return; + } + + qCDebugNC(EWSRES_AGENTIF_LOG) << "retrieveItem: done"; + itemRetrieved(item); +} +#endif + +void EwsResource::reloadConfig() +{ + mSubManager.reset(nullptr); + mEwsClient.setUrl(mSettings->baseUrl()); + mSettings->requestPassword(mPassword, false); + if (mSettings->domain().isEmpty()) { + mEwsClient.setCredentials(mSettings->username(), mPassword); + } else { + mEwsClient.setCredentials(mSettings->domain() + QChar::fromLatin1('\\') + mSettings->username(), mPassword); + } + mSettings->save(); + resetUrl(); +} + +void EwsResource::configure(WId windowId) +{ + QPointer dlg = new ConfigDialog(this, mEwsClient, windowId); + if (dlg->exec()) { + reloadConfig(); + } + delete dlg; +} + +void EwsResource::fetchFoldersJobFinished(KJob *job) +{ + Q_EMIT status(Idle, i18nc("@info:status The resource is ready", "Ready")); + 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) +{ + Q_EMIT status(Idle, i18nc("@info:status The resource is ready", "Ready")); + 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()); + + 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()); + Q_EMIT status(Idle, i18nc("@info:status The resource is ready", "Ready")); +} + +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, SIGNAL(result(KJob*)), SLOT(itemChangeRequestFinished(KJob*))); + 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; + + Q_FOREACH (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, SIGNAL(result(KJob*)), SLOT(itemMoveRequestFinished(KJob*))); + 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())); + 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 { + 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; + + Q_FOREACH (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 { + 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(EwsId::List folders) +{ + Q_FOREACH (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, SIGNAL(result(KJob*)), SLOT(foldersModifiedCollectionSyncFinished(KJob*))); + } + +} + +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, [this](KJob * job) { + if (job->error()) { + qCWarningNC(EWSRES_LOG) << "Special folders fetch failed:" << job->errorString(); + } + }); + job->start(); +} + +void EwsResource::specialFoldersCollectionsRetrieved(const Collection::List &folders) +{ + QStringList queryItemNames; + EwsId::List queryItems; + + Q_FOREACH (const SpecialFolders &sf, 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; + } + + 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; + Q_FOREACH (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.toAscii(), 9).toBase64())); + mSettings->save(); +} + +void EwsResource::doSetOnline(bool online) +{ + if (online) { + resetUrl(); + } 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()); + } +} + +AKONADI_RESOURCE_MAIN(EwsResource) diff --git a/resources/ews/ewsresource.desktop.cmake b/resources/ews/ewsresource.desktop.cmake new file mode 100644 --- /dev/null +++ b/resources/ews/ewsresource.desktop.cmake @@ -0,0 +1,10 @@ +[Desktop Entry] +Name=Microsoft Exchange Server (EWS) +Comment="Provides access to mail, appointments tasks and contacts of a Microsoft Exchange server using EWS" +Type=AkonadiResource +Exec=akonadi_ews_resource +Icon=akonadi-ews + +X-Akonadi-MimeTypes=text/directory,message/rfc822,text/calendar,application/x-vnd.kde.contactgroup,application/x-vnd.akonadi.calendar.event,application/x-vnd.akonadi.calendar.todo +X-Akonadi-Capabilities=Resource,NeedsNetwork,@EWS_MTA_CAPABILITIES@ +X-Akonadi-Identifier=akonadi_ews_resource diff --git a/resources/ews/ewsresource.kcfg b/resources/ews/ewsresource.kcfg new file mode 100644 --- /dev/null +++ b/resources/ews/ewsresource.kcfg @@ -0,0 +1,65 @@ + + + + + + + The URL of the Microsoft Exchange server, should be something like https://myserver.org/EWS/Exchange.asmx + The URL of the Microsoft Exchange server, should be something like https://myserver.org/EWS/Exchange.asmx + + + + The username that is used to log into the Microsoft Exchange server + The username that is used to log into the Microsoft Exchange server + + + + The Active Directory domain the user belongs to + The Active Directory domain the user belongs to + + + + true + + + + The primary e-mail address of this account + The primary e-mail address of this account + + + + Attempt to automatically discover Exchange server address + Attempts to find out the Microsoft Exchange server address relevant to the supplied e-mail address. + + + + Sets the interval for checking for new mail + Sets the interval for checking for new mail. + + + + + + + + + + default + + + + true + + + + + + + + + + diff --git a/resources/ews/ewsresource_debug.h b/resources/ews/ewsresource_debug.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewsresource_debug.h @@ -0,0 +1,80 @@ +/* + 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 EWSRESOURCE_DEBUG_H +#define EWSRESOURCE_DEBUG_H + +#include + +#include +#include + +#include "ewsclient.h" +#include "ewsclient_debug.h" +#include "ewsres_agentif_debug.h" +#include "ewsres_debug.h" + +inline QDebug operator<<(QDebug debug, const Akonadi::Item::List &items) +{ + QDebugStateSaver saver(debug); + QStringList itemStrs; + Q_FOREACH (const Akonadi::Item &item, items) { + itemStrs.append(ewsHash(item.remoteId())); + } + debug.nospace().noquote() << "Akonadi::Item::List(" << itemStrs.join(QChar::fromLatin1(',')) << ")"; + return debug.maybeSpace(); +} + +inline QDebug operator<<(QDebug debug, const Akonadi::Item &item) +{ + QDebugStateSaver saver(debug); + debug.nospace().noquote() << "Akonadi::Item(" << ewsHash(item.remoteId()) << ")"; + return debug.maybeSpace(); +} + +inline QDebug operator<<(QDebug debug, const Akonadi::Collection::List &cols) +{ + QDebugStateSaver saver(debug); + QStringList itemStrs; + Q_FOREACH (const Akonadi::Collection &col, cols) { + itemStrs.append(EwsClient::folderHash.value(col.remoteId(), ewsHash(col.remoteId()))); + } + debug.nospace().noquote() << "Akonadi::Collection::List(" << itemStrs.join(QChar::fromLatin1(',')) << ")"; + return debug.maybeSpace(); +} + +inline QDebug operator<<(QDebug debug, const Akonadi::Collection &col) +{ + QDebugStateSaver saver(debug); + debug.nospace().noquote() << "Akonadi::Collection(" << EwsClient::folderHash.value(col.remoteId(), ewsHash(col.remoteId())) << ")"; + return debug.maybeSpace(); +} + +inline QDebug operator<<(QDebug debug, const QSet &items) +{ + QDebugStateSaver saver(debug); + QStringList itemStrs; + Q_FOREACH (const QByteArray &item, items) { + itemStrs.append(QString::fromLatin1(item)); + } + debug.nospace().noquote() << "QSet(" << itemStrs.join(QChar::fromLatin1(',')) << ")"; + return debug.maybeSpace(); +} + +#endif diff --git a/resources/ews/ewssubscribedfoldersjob.h b/resources/ews/ewssubscribedfoldersjob.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewssubscribedfoldersjob.h @@ -0,0 +1,52 @@ +/* + 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 EWSSUBSCRIBEDFOLDERSJOB_H +#define EWSSUBSCRIBEDFOLDERSJOB_H + +#include "ewsjob.h" +#include "ewsid.h" + +class EwsClient; +class Settings; + +class EwsSubscribedFoldersJob : public EwsJob +{ + Q_OBJECT +public: + EwsSubscribedFoldersJob(EwsClient &client, Settings *settings, QObject *parent); + ~EwsSubscribedFoldersJob(); + + void start() override; + + EwsId::List folders() + { + return mFolders; + }; + + static const EwsId::List &defaultSubscriptionFolders(); +private Q_SLOTS: + void verifySubFoldersRequestFinished(KJob *job); +private: + EwsId::List mFolders; + EwsClient &mClient; + Settings *mSettings; +}; + +#endif diff --git a/resources/ews/ewssubscribedfoldersjob.cpp b/resources/ews/ewssubscribedfoldersjob.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewssubscribedfoldersjob.cpp @@ -0,0 +1,101 @@ +/* + 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 "ewssubscribedfoldersjob.h" + +#include "ewsclient.h" +#include "ewsgetfolderrequest.h" +#include "settings.h" +#include "ewsresource_debug.h" + +EwsSubscribedFoldersJob::EwsSubscribedFoldersJob(EwsClient &client, Settings *settings, QObject *parent) + : EwsJob(parent), mClient(client), mSettings(settings) +{ + +} + +EwsSubscribedFoldersJob::~EwsSubscribedFoldersJob() +{ +} + +void EwsSubscribedFoldersJob::start() +{ + EwsId::List ids; + + // Before subscribing make sure the subscription list doesn't contain invalid folders. + // Do this also for the default list in order to transform the distinguished IDs into real ones. + if (mSettings->serverSubscriptionList() == QStringList() << QStringLiteral("default")) { + ids = defaultSubscriptionFolders(); + } else { + Q_FOREACH (const QString &id, mSettings->serverSubscriptionList()) { + ids << EwsId(id); + } + } + + EwsGetFolderRequest *req = new EwsGetFolderRequest(mClient, this); + req->setFolderShape(EwsFolderShape(EwsShapeIdOnly)); + req->setFolderIds(ids); + req->setProperty("ids", QVariant::fromValue(ids)); + connect(req, &EwsRequest::result, this, &EwsSubscribedFoldersJob::verifySubFoldersRequestFinished); + req->start(); +} + +void EwsSubscribedFoldersJob::verifySubFoldersRequestFinished(KJob *job) +{ + if (!job->error()) { + EwsGetFolderRequest *req = qobject_cast(job); + Q_ASSERT(req); + + mFolders.clear(); + EwsId::List sourceIds = req->property("ids").value(); + QStringList idList; + + Q_ASSERT(req->responses().size() == sourceIds.size()); + + auto it = sourceIds.cbegin(); + + Q_FOREACH (const EwsGetFolderRequest::Response &resp, req->responses()) { + if (resp.isSuccess()) { + // Take just the id without the change key as the actual folder version is irrelevant + // here + QString id = resp.folder()[EwsFolderFieldFolderId].value().id(); + mFolders << EwsId(id); + idList << id; + } else { + qCWarningNC(EWSRES_LOG) << QStringLiteral("Invalid folder %1 - skipping").arg(it->id()); + } + it++; + } + + // Once verified write the final list back to the configuration. + mSettings->setServerSubscriptionList(idList); + } else { + setErrorMsg(job->errorString(), job->error()); + } + emitResult(); +} + +const EwsId::List &EwsSubscribedFoldersJob::defaultSubscriptionFolders() +{ + static const EwsId::List list = {EwsId(EwsDIdInbox), EwsId(EwsDIdCalendar), EwsId(EwsDIdTasks), + EwsId(EwsDIdContacts)}; + + return list; +} + diff --git a/resources/ews/ewssubscriptionmanager.h b/resources/ews/ewssubscriptionmanager.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewssubscriptionmanager.h @@ -0,0 +1,114 @@ +/* + 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 EWSSUBSCRIPTIONMANAGER_H +#define EWSSUBSCRIPTIONMANAGER_H + +#include +#include +#include + +#include "ewsid.h" + +class EwsClient; +class KJob; +class EwsEventRequestBase; +class Settings; + +/** + * @brief Mailbox update subscription manager class + * + * This class is responsible for retrieving update notifications from the Exchange server. + * + * The Exchange server has the ability to incrementally inform the client about changes made to + * selected folders in the mailbox. Each update informs about creation, modification or removal + * of an item or folder. Additionally Exchange has the ability to notify about free/busy status + * updates. + * + * Notifications can be delivered in 3 ways: + * - pull (i.e. polling) - the client needs to periodically question the server. + * - push - the server issues a callback connection to the client with events (not supported) + * - streaming - a combination of pull and push, where the client makes the connection, but the + * server keeps it open for a specified period of time and keeps delivering events + * over this connection (supported since Exchange 2010 SP2). + * + * The responsibility of this class is to retrieve and act upon change events from the Exchange + * server. The current implementation is simplified: + * - when an item update is received the folder containing the update is asked to synchronize + * itself. + * - when a folder update is received a full collection tree sync is performed. + * + * The above implementation has a major drawback in that operations performed by the resource + * itself are also notified back as update events. This means that when for ex. an item is deleted + * it is removed from Akonadi database, but subsequently a delete event is received which will try + * to delete an item that has already been deleted from Akonadi. + * + * To reduce such feedback loops the class implements a queued update mechanism. Each time an + * operation is performed on the mailbox the resource class is responsible for informing the + * subscription manager about it by adding an entry about the performed operation and its subject. + * The subscription manager will in turn filter out update events that refer to oprerations that + * have already been made. + */ +class EwsSubscriptionManager : public QObject +{ + Q_OBJECT +public: + EwsSubscriptionManager(EwsClient &client, const EwsId &rootId, Settings *settings, QObject *parent); + virtual ~EwsSubscriptionManager(); + void start(); + void queueUpdate(EwsEventType type, const QString &id, const QString &changeKey); +Q_SIGNALS: + void foldersModified(EwsId::List folders); + void folderTreeModified(); + void fullSyncRequested(); + void connectionError(); +private Q_SLOTS: + void subscribeRequestFinished(KJob *job); + void verifySubFoldersRequestFinished(KJob *job); + void getEventsRequestFinished(KJob *job); + void streamingEventsReceived(KJob *job); + void getEvents(); + void streamingConnectionTimeout(); +private: + void cancelSubscription(); + void setupSubscription(); + void setupSubscriptionReq(const EwsId::List &ids); + void reset(); + void resetSubscription(); + void processEvents(EwsEventRequestBase *req, bool finished); + + struct UpdateItem { + EwsEventType type; + QString changeKey; + }; + + EwsClient &mEwsClient; + QTimer mPollTimer; + EwsId mMsgRootId; + + QSet mUpdatedFolderIds; + bool mFolderTreeChanged; + bool mStreamingEvents; + QMultiHash mQueuedUpdates; + QTimer mStreamingTimer; + EwsEventRequestBase *mEventReq; + Settings *mSettings; +}; + +#endif diff --git a/resources/ews/ewssubscriptionmanager.cpp b/resources/ews/ewssubscriptionmanager.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewssubscriptionmanager.cpp @@ -0,0 +1,324 @@ +/* + 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 "ewssubscriptionmanager.h" + +#include "ewsresource_debug.h" +#include "ewsgeteventsrequest.h" +#include "ewsgetfolderrequest.h" +#include "ewsgetstreamingeventsrequest.h" +#include "ewssubscribedfoldersjob.h" +#include "ewssubscriberequest.h" +#include "ewsunsubscriberequest.h" +#include "settings.h" + +// TODO: Allow customization +static Q_CONSTEXPR uint pollInterval = 10; /* seconds */ + +static Q_CONSTEXPR uint streamingTimeout = 30; /* minutes */ + +static Q_CONSTEXPR uint streamingConnTimeout = 60; /* seconds */ + +EwsSubscriptionManager::EwsSubscriptionManager(EwsClient &client, const EwsId &rootId, + Settings *settings, QObject *parent) + : QObject(parent), mEwsClient(client), mPollTimer(this), mMsgRootId(rootId), mFolderTreeChanged(false), + mEventReq(nullptr), mSettings(settings) +{ + mStreamingEvents = mEwsClient.serverVersion().supports(EwsServerVersion::StreamingSubscription); + mStreamingTimer.setInterval(streamingConnTimeout * 1000); + mStreamingTimer.setSingleShot(true); + connect(&mStreamingTimer, SIGNAL(timeout()), this, SLOT(streamingConnectionTimeout())); +} + +EwsSubscriptionManager::~EwsSubscriptionManager() +{ + cancelSubscription(); +} + +void EwsSubscriptionManager::start() +{ + // Set-up change notification subscription (if needed) + if (mSettings->eventSubscriptionId().isEmpty()) { + setupSubscription(); + } else { + reset(); + } + + if (!mStreamingEvents) { + mPollTimer.setInterval(pollInterval * 1000); + mPollTimer.setSingleShot(false); + connect(&mPollTimer, &QTimer::timeout, this, &EwsSubscriptionManager::getEvents); + } +} + +void EwsSubscriptionManager::cancelSubscription() +{ + if (!mSettings->eventSubscriptionId().isEmpty()) { + QPointer req = new EwsUnsubscribeRequest(mEwsClient, this); + req->setSubscriptionId(mSettings->eventSubscriptionId()); + req->exec(); + mSettings->setEventSubscriptionId(QString()); + mSettings->setEventSubscriptionWatermark(QString()); + mSettings->save(); + } +} + +void EwsSubscriptionManager::setupSubscription() +{ + EwsId::List ids; + + EwsSubscribedFoldersJob *job = new EwsSubscribedFoldersJob(mEwsClient, mSettings, this); + connect(job, &EwsRequest::result, this, &EwsSubscriptionManager::verifySubFoldersRequestFinished); + job->start(); +} + +void EwsSubscriptionManager::verifySubFoldersRequestFinished(KJob *job) +{ + if (!job->error()) { + EwsSubscribedFoldersJob *folderJob = qobject_cast(job); + Q_ASSERT(folderJob); + + setupSubscriptionReq(folderJob->folders()); + } else { + Q_EMIT connectionError(); + } +} + +void EwsSubscriptionManager::setupSubscriptionReq(const EwsId::List &ids) +{ + EwsSubscribeRequest *req = new EwsSubscribeRequest(mEwsClient, this); + //req->setAllFolders(true); + QList events; + events << EwsNewMailEvent; + events << EwsMovedEvent; + events << EwsCopiedEvent; + events << EwsModifiedEvent; + events << EwsDeletedEvent; + events << EwsCreatedEvent; + req->setEventTypes(events); + if (mStreamingEvents) { + req->setType(EwsSubscribeRequest::StreamingSubscription); + } else { + req->setType(EwsSubscribeRequest::PullSubscription); + } + req->setFolderIds(ids); + req->setAllFolders(false); + connect(req, &EwsRequest::result, this, &EwsSubscriptionManager::subscribeRequestFinished); + req->start(); +} + +void EwsSubscriptionManager::reset() +{ + mPollTimer.stop(); + getEvents(); + if (!mStreamingEvents) { + mPollTimer.start(); + } +} + +void EwsSubscriptionManager::resetSubscription() +{ + mPollTimer.stop(); + cancelSubscription(); + setupSubscription(); +} + +void EwsSubscriptionManager::subscribeRequestFinished(KJob *job) +{ + if (!job->error()) { + EwsSubscribeRequest *req = qobject_cast(job); + if (req) { + mSettings->setEventSubscriptionId(req->response().subscriptionId()); + if (mStreamingEvents) { + getEvents(); + } else { + mSettings->setEventSubscriptionWatermark(req->response().watermark()); + getEvents(); + mPollTimer.start(); + } + mSettings->save(); + } + } else { + Q_EMIT connectionError(); + } +} + +void EwsSubscriptionManager::getEvents() +{ + if (mStreamingEvents) { + EwsGetStreamingEventsRequest *req = new EwsGetStreamingEventsRequest(mEwsClient, this); + req->setSubscriptionId(mSettings->eventSubscriptionId()); + req->setTimeout(streamingTimeout); + connect(req, &EwsRequest::result, this, &EwsSubscriptionManager::getEventsRequestFinished); + connect(req, &EwsGetStreamingEventsRequest::eventsReceived, this, + &EwsSubscriptionManager::streamingEventsReceived); + req->start(); + mEventReq = req; + mStreamingTimer.start(); + } else { + EwsGetEventsRequest *req = new EwsGetEventsRequest(mEwsClient, this); + req->setSubscriptionId(mSettings->eventSubscriptionId()); + req->setWatermark(mSettings->eventSubscriptionWatermark()); + connect(req, &EwsRequest::result, this, &EwsSubscriptionManager::getEventsRequestFinished); + req->start(); + mEventReq = req; + } +} + +void EwsSubscriptionManager::getEventsRequestFinished(KJob *job) +{ + mStreamingTimer.stop(); + + mEventReq->deleteLater(); + mEventReq = nullptr; + + EwsEventRequestBase *req = qobject_cast(job); + if (!req) { + qCWarningNC(EWSRES_LOG) << QStringLiteral("Invalid EwsEventRequestBase job object."); + reset(); + return; + } + + if ((!req->responses().isEmpty()) && + ((req->responses()[0].responseCode() == QStringLiteral("ErrorInvalidSubscription")) || + (req->responses()[0].responseCode() == QStringLiteral("ErrorSubscriptionNotFound")))) { + mSettings->setEventSubscriptionId(QString()); + mSettings->setEventSubscriptionWatermark(QString()); + mSettings->save(); + resetSubscription(); + return; + } + + if (!job->error()) { + processEvents(req, true); + if (mStreamingEvents) { + getEvents(); + } + } else { + reset(); + } +} + +void EwsSubscriptionManager::streamingEventsReceived(KJob *job) +{ + mStreamingTimer.stop(); + + EwsEventRequestBase *req = qobject_cast(job); + if (!req) { + qCWarningNC(EWSRES_LOG) << QStringLiteral("Invalid EwsEventRequestBase job object."); + reset(); + return; + } + + if (!job->error()) { + processEvents(req, false); + mStreamingTimer.start(); + } +} + +void EwsSubscriptionManager::streamingConnectionTimeout() +{ + if (mEventReq) { + qCWarningNC(EWSRES_LOG) << QStringLiteral("Streaming request timeout - restarting"); + mEventReq->deleteLater(); + mEventReq = nullptr; + getEvents(); + } +} + +void EwsSubscriptionManager::processEvents(EwsEventRequestBase *req, bool finished) +{ + bool moreEvents = false; + + Q_FOREACH (const EwsGetEventsRequest::Response &resp, req->responses()) { + Q_FOREACH (const EwsGetEventsRequest::Notification &nfy, resp.notifications()) { + Q_FOREACH (const EwsGetEventsRequest::Event &event, nfy.events()) { + + bool skip = false; + EwsId id = event.itemId(); + for (auto it = mQueuedUpdates.find(id.id()); it != mQueuedUpdates.end(); ++it) { + if (it->type == event.type() + && (it->type == EwsDeletedEvent || it->changeKey == id.changeKey())) { + qCDebugNC(EWSRES_LOG) << QStringLiteral("Skipped queued update type %1 for item %2"); + skip = true; + mQueuedUpdates.erase(it); + break; + } + } + + mSettings->setEventSubscriptionWatermark(event.watermark()); + if (!skip) { + switch (event.type()) { + case EwsCopiedEvent: + case EwsMovedEvent: + if (!event.itemIsFolder()) { + mUpdatedFolderIds.insert(event.oldParentFolderId()); + } + /* fall through */ + case EwsCreatedEvent: + case EwsDeletedEvent: + case EwsModifiedEvent: + case EwsNewMailEvent: + if (event.itemIsFolder()) { + mFolderTreeChanged = true; + } else { + mUpdatedFolderIds.insert(event.parentFolderId()); + } + break; + case EwsStatusEvent: + // Do nothing + break; + default: + break; + } + } + } + if (nfy.hasMoreEvents()) { + moreEvents = true; + } + } + if (mStreamingEvents) { + EwsGetStreamingEventsRequest *req2 = qobject_cast(req); + if (req2) { + req2->eventsProcessed(resp); + } + } + } + + if (moreEvents && finished) { + getEvents(); + } else { + if (mFolderTreeChanged) { + qCDebugNC(EWSRES_LOG) << QStringLiteral("Found modified folder tree"); + Q_EMIT folderTreeModified(); + mFolderTreeChanged = false; + } + if (!mUpdatedFolderIds.isEmpty()) { + qCDebugNC(EWSRES_LOG) << QStringLiteral("Found %1 modified folders") + .arg(mUpdatedFolderIds.size()); + Q_EMIT foldersModified(mUpdatedFolderIds.toList()); + mUpdatedFolderIds.clear(); + } + } +} + +void EwsSubscriptionManager::queueUpdate(EwsEventType type, const QString &id, const QString &changeKey) +{ + mQueuedUpdates.insert(id, {type, changeKey}); +} diff --git a/resources/ews/ewssubscriptionwidget.h b/resources/ews/ewssubscriptionwidget.h new file mode 100644 --- /dev/null +++ b/resources/ews/ewssubscriptionwidget.h @@ -0,0 +1,47 @@ +/* + 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 EWSSUBSCRIPTIONWIDGET_H +#define EWSSUBSCRIPTIONWIDGET_H + +#include +#include + +#include "ewsid.h" + +class EwsSubscriptionWidgetPrivate; +class EwsClient; +class Settings; + +class EwsSubscriptionWidget : public QWidget +{ + Q_OBJECT +public: + EwsSubscriptionWidget(EwsClient &client, Settings *settings, QWidget *parent); + ~EwsSubscriptionWidget(); + + QStringList subscribedList() const; + bool subscribedListValid() const; + bool subscriptionEnabled() const; +private: + QScopedPointer d_ptr; + Q_DECLARE_PRIVATE(EwsSubscriptionWidget) +}; + +#endif diff --git a/resources/ews/ewssubscriptionwidget.cpp b/resources/ews/ewssubscriptionwidget.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/ewssubscriptionwidget.cpp @@ -0,0 +1,389 @@ +/* + 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 "ewssubscriptionwidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "ewsclient.h" +#include "ewsfindfolderrequest.h" +#include "ewssubscribedfoldersjob.h" +#include "settings.h" + +class EwsSubscriptionFilterModel : public KRecursiveFilterProxyModel +{ + Q_OBJECT +public: + EwsSubscriptionFilterModel(QObject *parent = nullptr); + ~EwsSubscriptionFilterModel(); +public Q_SLOTS: + void setFilterSelected(bool enabled); +protected: + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; +private: + bool hasCheckedChildren(QModelIndex index) const; + bool mFilterSelected; +}; + +class EwsSubscriptionWidgetPrivate : public QObject +{ + Q_OBJECT +public: + EwsSubscriptionWidgetPrivate(EwsClient &client, Settings *settings, QObject *parent); + ~EwsSubscriptionWidgetPrivate(); + + enum TreeModelRoles { + ItemIdRole = Qt::UserRole + 1 + }; + + void populateFolderTree(); + +public Q_SLOTS: + void enableCheckBoxToggled(bool checked); + void reloadFolderList(bool); + void resetSelection(bool); + void readFolderListFinished(KJob *job); + void subscribedFoldersJobFinished(KJob *job); + void treeItemChanged(QStandardItem *item); + void filterTextChanged(const QString &text); +public: + bool mEnabled; + QCheckBox *mEnableCheckBox; + QTreeView *mFolderTreeView; + QWidget *mSubContainer; + QPushButton *mRefreshButton; + EwsClient &mClient; + KMessageWidget *mMsgWidget; + QStandardItemModel *mFolderTreeModel; + QHash mFolderItemHash; + int mFolderListPendingRequests; + EwsFolder::List mFolders; + EwsId::List mSubscribedIds; + EwsId::List mOrigSubscribedIds; + bool mSubscribedIdsRetrieved; + EwsSubscriptionFilterModel *mFilterModel; + Settings *mSettings; + + EwsSubscriptionWidget *q_ptr; + Q_DECLARE_PUBLIC(EwsSubscriptionWidget) +}; + +EwsSubscriptionFilterModel::EwsSubscriptionFilterModel(QObject *parent) + : KRecursiveFilterProxyModel(parent), mFilterSelected(false) +{ +} + +EwsSubscriptionFilterModel::~EwsSubscriptionFilterModel() +{ +} + +bool EwsSubscriptionFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + bool show = true; + if (mFilterSelected) { + QModelIndex sourceIndex = sourceModel()->index(sourceRow, 0, sourceParent); + + show = sourceIndex.data(Qt::CheckStateRole).toInt() == Qt::Checked; + show |= hasCheckedChildren(sourceIndex); + } + + if (!show) { + return false; + } else { + return KRecursiveFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent); + } +} + +bool EwsSubscriptionFilterModel::hasCheckedChildren(QModelIndex index) const +{ + QModelIndex child; + int row = 0; + child = index.child(row, 0); + while (child.isValid()) { + if (child.data(Qt::CheckStateRole).toInt() == Qt::Checked) { + return true; + } else if (hasCheckedChildren(child)) { + return true; + } + child = index.child(++row, 0); + } + + return false; +} + +void EwsSubscriptionFilterModel::setFilterSelected(bool enabled) +{ + mFilterSelected = enabled; + invalidateFilter(); +} + +EwsSubscriptionWidgetPrivate::EwsSubscriptionWidgetPrivate(EwsClient &client, Settings *settings, + QObject *parent) + : QObject(parent), mClient(client), mSubscribedIdsRetrieved(false), mSettings(settings) +{ +} + +EwsSubscriptionWidgetPrivate::~EwsSubscriptionWidgetPrivate() +{ +} + +void EwsSubscriptionWidgetPrivate::enableCheckBoxToggled(bool checked) +{ + mSubContainer->setEnabled(checked); +} + +void EwsSubscriptionWidgetPrivate::reloadFolderList(bool) +{ + if (mClient.isConfigured()) { + EwsFindFolderRequest *req = new EwsFindFolderRequest(mClient, this); + EwsFolderShape shape(EwsShapeIdOnly); + shape << EwsPropertyField(QStringLiteral("folder:DisplayName")); + shape << EwsPropertyField(QStringLiteral("folder:ParentFolderId")); + req->setFolderShape(shape); + req->setParentFolderId(EwsId(EwsDIdMsgFolderRoot)); + connect(req, &EwsRequest::result, this, &EwsSubscriptionWidgetPrivate::readFolderListFinished); + req->start(); + mFolderListPendingRequests = 1; + if (!mSubscribedIdsRetrieved) { + EwsSubscribedFoldersJob *job = new EwsSubscribedFoldersJob(mClient, mSettings, this); + connect(job, &EwsRequest::result, this, &EwsSubscriptionWidgetPrivate::subscribedFoldersJobFinished); + job->start(); + mFolderListPendingRequests++; + } + mRefreshButton->setEnabled(false); + + } else { + mMsgWidget->setText(i18nc("@info", "Exchange server not configured.")); + mMsgWidget->setMessageType(KMessageWidget::Error); + mMsgWidget->animatedShow(); + } +} + +void EwsSubscriptionWidgetPrivate::readFolderListFinished(KJob *job) +{ + if (job->error()) { + mMsgWidget->setText(i18nc("@info", "Failed to retrieve folder list.")); + mMsgWidget->setMessageType(KMessageWidget::Error); + mMsgWidget->animatedShow(); + mRefreshButton->setEnabled(true); + } else { + EwsFindFolderRequest *req = qobject_cast(job); + Q_ASSERT(req); + + mFolders = req->folders(); + + mFolderListPendingRequests--; + if (mFolderListPendingRequests == 0) { + mRefreshButton->setEnabled(true); + populateFolderTree(); + } + } +} + +void EwsSubscriptionWidgetPrivate::subscribedFoldersJobFinished(KJob *job) +{ + if (job->error()) { + mMsgWidget->setText(i18nc("@info", "Failed to retrieve folder list.")); + mMsgWidget->setMessageType(KMessageWidget::Error); + mMsgWidget->animatedShow(); + mRefreshButton->setEnabled(true); + } else { + EwsSubscribedFoldersJob *req = qobject_cast(job); + Q_ASSERT(req); + + mSubscribedIds = req->folders(); + + mFolderListPendingRequests--; + mOrigSubscribedIds = mSubscribedIds; + mSubscribedIdsRetrieved = true; + if (mFolderListPendingRequests == 0) { + mRefreshButton->setEnabled(true); + populateFolderTree(); + } + } + +} + +void EwsSubscriptionWidgetPrivate::populateFolderTree() +{ + mFolderTreeModel->clear(); + mFolderItemHash.clear(); + + Q_FOREACH (const EwsFolder &folder, mFolders) { + QStandardItem *item = new QStandardItem(folder[EwsFolderFieldDisplayName].toString()); + item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); + item->setCheckable(true); + EwsId id = folder[EwsFolderFieldFolderId].value(); + item->setData(id.id(), ItemIdRole); + if (mSubscribedIds.contains(EwsId(id.id()))) { + item->setCheckState(Qt::Checked); + } + EwsId parentId = folder[EwsFolderFieldParentFolderId].value(); + if (parentId.type() != EwsId::Unspecified) { + QStandardItem *parentItem = mFolderItemHash.value(parentId.id()); + if (parentItem) { + parentItem->appendRow(item); + } + } + mFolderItemHash.insert(id.id(), item); + } + + Q_FOREACH (QStandardItem* item, mFolderItemHash) { + if (!item->parent()) { + mFolderTreeModel->appendRow(item); + } + } +} + +void EwsSubscriptionWidgetPrivate::treeItemChanged(QStandardItem *item) +{ + EwsId id = EwsId(item->data(ItemIdRole).toString()); + if (item->checkState() == Qt::Checked) { + mSubscribedIds += id; + } else { + mSubscribedIds.removeOne(id); + } +} + +void EwsSubscriptionWidgetPrivate::filterTextChanged(const QString &text) +{ + mFilterModel->setFilterFixedString(text); +} + +void EwsSubscriptionWidgetPrivate::resetSelection(bool) +{ + mSubscribedIds = mOrigSubscribedIds; + populateFolderTree(); +} + +EwsSubscriptionWidget::EwsSubscriptionWidget(EwsClient &client, Settings *settings, QWidget *parent) + : QWidget(parent), d_ptr(new EwsSubscriptionWidgetPrivate(client, settings, this)) +{ + Q_D(EwsSubscriptionWidget); + + d->mEnabled = d->mSettings->serverSubscription(); + + QVBoxLayout *topLayout = new QVBoxLayout(this); + + d->mMsgWidget = new KMessageWidget(this); + d->mMsgWidget->setVisible(false); + + d->mEnableCheckBox = new QCheckBox(i18nc("@option:check", "Enable server-side subscriptions"), this); + d->mEnableCheckBox->setChecked(d->mEnabled); + + d->mSubContainer = new QWidget(this); + QVBoxLayout *subContainerLayout = new QVBoxLayout(d->mSubContainer); + subContainerLayout->setMargin(0); + + QLineEdit *filterLineEdit = new QLineEdit(this); + filterLineEdit->setPlaceholderText(i18nc("@label:textbox", "Filter folders")); + + QWidget *treeContainer = new QWidget(this); + QHBoxLayout *treeContainerLayout = new QHBoxLayout(treeContainer); + treeContainerLayout->setMargin(0); + + d->mFolderTreeView = new QTreeView(this); + d->mFolderTreeModel = new QStandardItemModel(this); + d->mFilterModel = new EwsSubscriptionFilterModel(this); + d->mFilterModel->setSourceModel(d->mFolderTreeModel); + d->mFilterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + d->mFilterModel->sort(0, Qt::AscendingOrder); + d->mFolderTreeView->setModel(d->mFilterModel); + d->mFolderTreeView->header()->hide(); + + QWidget *buttonContainer = new QWidget(this); + QVBoxLayout *buttonContainerLayout = new QVBoxLayout(buttonContainer); + buttonContainerLayout->setMargin(0); + + d->mRefreshButton = new QPushButton(this); + d->mRefreshButton->setText(i18nc("@action:button", "Reload &List")); + + QPushButton *resetButton = new QPushButton(this); + resetButton->setText(i18nc("@action:button", "&Reset")); + + buttonContainerLayout->addWidget(d->mRefreshButton); + buttonContainerLayout->addWidget(resetButton); + buttonContainerLayout->addStretch(); + + treeContainerLayout->addWidget(d->mFolderTreeView); + treeContainerLayout->addWidget(buttonContainer); + + QCheckBox *subOnlyCheckBox = new QCheckBox(i18nc("@option:check", "Subscribed only"), this); + + subContainerLayout->addWidget(filterLineEdit); + subContainerLayout->addWidget(treeContainer); + subContainerLayout->addWidget(subOnlyCheckBox); + + topLayout->addWidget(d->mMsgWidget); + topLayout->addWidget(d->mEnableCheckBox); + topLayout->addWidget(d->mSubContainer); + + connect(d->mEnableCheckBox, &QCheckBox::toggled, d, &EwsSubscriptionWidgetPrivate::enableCheckBoxToggled); + connect(d->mRefreshButton, &QPushButton::clicked, d, &EwsSubscriptionWidgetPrivate::reloadFolderList); + connect(resetButton, &QPushButton::clicked, d, &EwsSubscriptionWidgetPrivate::resetSelection); + connect(d->mFolderTreeModel, &QStandardItemModel::itemChanged, d, &EwsSubscriptionWidgetPrivate::treeItemChanged); + connect(filterLineEdit, &QLineEdit::textChanged, d, &EwsSubscriptionWidgetPrivate::filterTextChanged); + connect(subOnlyCheckBox, &QCheckBox::toggled, d->mFilterModel, &EwsSubscriptionFilterModel::setFilterSelected); + + d->enableCheckBoxToggled(d->mEnabled); + d->reloadFolderList(false); +} + +EwsSubscriptionWidget::~EwsSubscriptionWidget() +{ +} + +QStringList EwsSubscriptionWidget::subscribedList() const +{ + Q_D(const EwsSubscriptionWidget); + + QStringList list; + Q_FOREACH (const EwsId &id, d->mSubscribedIds) { + list.append(id.id()); + } + + return list; +} + +bool EwsSubscriptionWidget::subscriptionEnabled() const +{ + Q_D(const EwsSubscriptionWidget); + + return d->mEnableCheckBox->isChecked(); +} + +bool EwsSubscriptionWidget::subscribedListValid() const +{ + Q_D(const EwsSubscriptionWidget); + + return d->mSubscribedIdsRetrieved; +} + +#include "ewssubscriptionwidget.moc" diff --git a/resources/ews/icons/akonadi-ews-128.png b/resources/ews/icons/akonadi-ews-128.png new file mode 100644 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 GIT binary patch literal 0 Hc$@ + + + + + + + + + + + hash + + action + computer + icons + theme + + + + + Danny Allen + + + + + Danny Allen + + + + + Danny Allen + + + + image/svg+xml + + + en + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/ews/mail/ewscreatemailjob.h b/resources/ews/mail/ewscreatemailjob.h new file mode 100644 --- /dev/null +++ b/resources/ews/mail/ewscreatemailjob.h @@ -0,0 +1,43 @@ +/* + 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. +*/ + +#ifndef EWSCREATEMAILJOB_H +#define EWSCREATEMAILJOB_H + +#include "ewscreateitemjob.h" + +class EwsCreateMailJob : public EwsCreateItemJob +{ + Q_OBJECT +public: + EwsCreateMailJob(EwsClient &client, const Akonadi::Item &item, + const Akonadi::Collection &collection, EwsTagStore *tagStore, EwsResource *parent); + virtual ~EwsCreateMailJob(); + virtual bool setSend(bool send = true) override; +protected: + virtual void doStart() override; +private Q_SLOTS: + void mailCreateFinished(KJob *job); + void mailCreateWorkaroundFinished(KJob *job); + void mailMoveWorkaroundFinished(KJob *job); +private: + bool mSend; +}; + +#endif diff --git a/resources/ews/mail/ewscreatemailjob.cpp b/resources/ews/mail/ewscreatemailjob.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/mail/ewscreatemailjob.cpp @@ -0,0 +1,237 @@ +/* + 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 "ewscreatemailjob.h" + +#include +#include +#include +#include +#include +#include + +#include "ewscreateitemrequest.h" +#include "ewsmoveitemrequest.h" +#include "ewspropertyfield.h" +#include "ewsmailhandler.h" +#include "ewsresource_debug.h" + +using namespace Akonadi; + +static const EwsPropertyField propPidMessageFlags(0x0e07, EwsPropTypeInteger); + +EwsCreateMailJob::EwsCreateMailJob(EwsClient &client, const Akonadi::Item &item, + const Akonadi::Collection &collection, EwsTagStore *tagStore, + EwsResource *parent) + : EwsCreateItemJob(client, item, collection, tagStore, parent), mSend(false) +{ +} +EwsCreateMailJob::~EwsCreateMailJob() +{ +} + +void EwsCreateMailJob::doStart() +{ + if (!mItem.hasPayload()) { + setErrorMsg(QStringLiteral("Expected MIME message payload")); + emitResult(); + } + + EwsCreateItemRequest *req = new EwsCreateItemRequest(mClient, this); + + KMime::Message::Ptr msg = mItem.payload(); + /* Exchange doesn't just store whatever MIME content that was sent to it - it will parse it and send + * further the version assembled back from the parsed parts. It seems that this parsing doesn't work well + * with the quoted-printable encoding, which KMail prefers. This results in malformed encoding, which the + * sender doesn't even see. + * As a workaround force encoding of the body (or in case of multipart - all parts) to Base64. */ + if (msg->contents().isEmpty()) { + msg->changeEncoding(KMime::Headers::CEbase64); + msg->contentTransferEncoding(true)->setEncoding(KMime::Headers::CEbase64); + } else { + Q_FOREACH (KMime::Content *content, msg->contents()) { + content->changeEncoding(KMime::Headers::CEbase64); + content->contentTransferEncoding(true)->setEncoding(KMime::Headers::CEbase64); + } + } + msg->assemble(); + QByteArray mimeContent = msg->encodedContent(true); + bool sentItemsCreateWorkaround = false; + EwsItem item; + item.setType(EwsItemTypeMessage); + item.setField(EwsItemFieldMimeContent, mimeContent); + if (!mSend) { + /* When creating items using the CreateItem request Exchange will by default mark the message + * as draft. Setting the extended property below causes the message to appear normally. */ + item.setProperty(propPidMessageFlags, QStringLiteral("1")); + const Akonadi::AgentInstance &inst = Akonadi::AgentManager::self()->instance(mCollection.resource()); + + /* WORKAROUND: The "Sent Items" folder is a little "special" when it comes to creating items. + * Unlike other folders when creating items there the creation date/time is always set to the + * current date/time instead of the value from the MIME Date header. This causes mail that + * was copied from other folders to appear with the current date/time instead of the original one. + * To work around this create the item in the "Drafts" folder first and then move it to "Sent Items". */ + if (mCollection == SpecialMailCollections::self()->collection(SpecialMailCollections::SentMail, inst)) { + qCInfoNC(EWSRES_LOG) << "Move to \"Sent Items\" detected - activating workaround."; + const Collection &draftColl = SpecialMailCollections::self()->collection(SpecialMailCollections::Drafts, inst); + req->setSavedFolderId(EwsId(draftColl.remoteId(), draftColl.remoteRevision())); + sentItemsCreateWorkaround = true; + } else { + req->setSavedFolderId(EwsId(mCollection.remoteId(), mCollection.remoteRevision())); + } + } + // Set flags + QHash propertyHash = EwsMailHandler::writeFlags(mItem.flags()); + for (auto it = propertyHash.cbegin(); it != propertyHash.cend(); ++it) { + if (!it.value().isNull()) { + if (it.key().type() == EwsPropertyField::ExtendedField) { + item.setProperty(it.key(), it.value()); + } else if (it.key().type() == EwsPropertyField::Field) { + /* TODO: Currently EwsItem distinguishes between regular fields and extended fields + * and keeps them in separate lists. Someday it will make more sense to unify them. + * Until that the code below needs to manually translate the field names into + * EwsItemField enum items. + */ + if (it.key().uri() == QStringLiteral("message:IsRead")) { + item.setField(EwsItemFieldIsRead, it.value()); + } + } + } + } + + populateCommonProperties(item); + + req->setItems(EwsItem::List() << item); + req->setMessageDisposition(mSend ? EwsDispSendOnly : EwsDispSaveOnly); + connect(req, &EwsCreateItemRequest::finished, this, + sentItemsCreateWorkaround ? &EwsCreateMailJob::mailCreateWorkaroundFinished : &EwsCreateMailJob::mailCreateFinished); + addSubjob(req); + req->start(); +} + +void EwsCreateMailJob::mailCreateFinished(KJob *job) +{ + EwsCreateItemRequest *req = qobject_cast(job); + if (job->error()) { + setErrorMsg(job->errorString()); + emitResult(); + return; + } + + if (!req) { + setErrorMsg(QStringLiteral("Invalid EwsCreateItemRequest job object")); + emitResult(); + return; + } + + if (req->responses().count() != 1) { + setErrorMsg(QStringLiteral("Invalid number of responses received from server.")); + emitResult(); + return; + } + + EwsCreateItemRequest::Response resp = req->responses().first(); + if (resp.isSuccess()) { + EwsId id = resp.itemId(); + mItem.setRemoteId(id.id()); + mItem.setRemoteRevision(id.changeKey()); + mItem.setParentCollection(mCollection); + } else { + setErrorMsg(i18n("Failed to create mail item")); + } + + emitResult(); +} + +void EwsCreateMailJob::mailCreateWorkaroundFinished(KJob *job) +{ + EwsCreateItemRequest *req = qobject_cast(job); + if (job->error()) { + setErrorMsg(job->errorString()); + emitResult(); + return; + } + + if (!req) { + setErrorMsg(QStringLiteral("Invalid EwsCreateItemRequest job object")); + emitResult(); + return; + } + + if (req->responses().count() != 1) { + setErrorMsg(QStringLiteral("Invalid number of responses received from server.")); + emitResult(); + return; + } + + EwsCreateItemRequest::Response resp = req->responses().first(); + if (resp.isSuccess()) { + EwsId id = resp.itemId(); + EwsMoveItemRequest *req = new EwsMoveItemRequest(mClient, this); + req->setItemIds(EwsId::List() << id); + req->setDestinationFolderId(EwsId(mCollection.remoteId())); + connect(req, &EwsCreateItemRequest::finished, this, &EwsCreateMailJob::mailMoveWorkaroundFinished); + addSubjob(req); + req->start(); + } else { + setErrorMsg(i18n("Failed to create mail item")); + emitResult(); + } +} + +void EwsCreateMailJob::mailMoveWorkaroundFinished(KJob *job) +{ + EwsMoveItemRequest *req = qobject_cast(job); + if (job->error()) { + setErrorMsg(job->errorString()); + emitResult(); + return; + } + + if (!req) { + setErrorMsg(QStringLiteral("Invalid EwsMoveItemRequest job object")); + emitResult(); + return; + } + + if (req->responses().count() != 1) { + setErrorMsg(QStringLiteral("Invalid number of responses received from server.")); + emitResult(); + return; + } + + EwsMoveItemRequest::Response resp = req->responses().first(); + if (resp.isSuccess()) { + EwsId id = resp.itemId(); + mItem.setRemoteId(id.id()); + mItem.setRemoteRevision(id.changeKey()); + mItem.setParentCollection(mCollection); + } else { + setErrorMsg(i18n("Failed to create mail item")); + } + + emitResult(); +} + + +bool EwsCreateMailJob::setSend(bool send) +{ + mSend = send; + return true; +} diff --git a/resources/ews/mail/ewsfetchmaildetailjob.h b/resources/ews/mail/ewsfetchmaildetailjob.h new file mode 100644 --- /dev/null +++ b/resources/ews/mail/ewsfetchmaildetailjob.h @@ -0,0 +1,35 @@ +/* + 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. +*/ + +#ifndef EWSFETCHMAILDETAILJOB_H +#define EWSFETCHMAILDETAILJOB_H + +#include "ewsfetchitemdetailjob.h" + +class EwsFetchMailDetailJob : public EwsFetchItemDetailJob +{ + Q_OBJECT +public: + EwsFetchMailDetailJob(EwsClient &client, QObject *parent, const Akonadi::Collection &collection); + virtual ~EwsFetchMailDetailJob(); +protected: + virtual void processItems(const QList &responses) override; +}; + +#endif diff --git a/resources/ews/mail/ewsfetchmaildetailjob.cpp b/resources/ews/mail/ewsfetchmaildetailjob.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/mail/ewsfetchmaildetailjob.cpp @@ -0,0 +1,170 @@ +/* + 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 "ewsfetchmaildetailjob.h" + +#include +#include +#include + +#include "ewsitemshape.h" +#include "ewsgetitemrequest.h" +#include "ewsmailbox.h" +#include "ewsmailhandler.h" +#include "ewsresource_debug.h" + +using namespace Akonadi; + +EwsFetchMailDetailJob::EwsFetchMailDetailJob(EwsClient &client, QObject *parent, const Akonadi::Collection &collection) + : EwsFetchItemDetailJob(client, parent, collection) +{ + EwsItemShape shape(EwsShapeIdOnly); + shape << EwsPropertyField(QStringLiteral("item:Subject")); + shape << EwsPropertyField(QStringLiteral("item:Importance")); + shape << EwsPropertyField(QStringLiteral("message:From")); + shape << EwsPropertyField(QStringLiteral("message:ToRecipients")); + shape << EwsPropertyField(QStringLiteral("message:CcRecipients")); + shape << EwsPropertyField(QStringLiteral("message:BccRecipients")); + shape << EwsPropertyField(QStringLiteral("item:Categories")); + shape << EwsPropertyField(QStringLiteral("item:DateTimeReceived")); + shape << EwsPropertyField(QStringLiteral("item:InReplyTo")); + shape << EwsPropertyField(QStringLiteral("message:References")); + shape << EwsPropertyField(QStringLiteral("message:ReplyTo")); + shape << EwsPropertyField(QStringLiteral("message:InternetMessageId")); + shape << EwsPropertyField(QStringLiteral("item:Size")); + Q_FOREACH (const EwsPropertyField &field, EwsMailHandler::flagsProperties()) { + shape << field; + } + Q_FOREACH (const EwsPropertyField &field, EwsItemHandler::tagsProperties()) { + shape << field; + } + mRequest->setItemShape(shape); +} + + +EwsFetchMailDetailJob::~EwsFetchMailDetailJob() +{ +} + +void EwsFetchMailDetailJob::processItems(const QList &responses) +{ + Item::List::iterator it = mChangedItems.begin(); + + Q_FOREACH (const EwsGetItemRequest::Response &resp, responses) { + Item &item = *it; + + if (!resp.isSuccess()) { + qCWarningNC(EWSRES_LOG) << QStringLiteral("Failed to fetch item %1").arg(item.remoteId()); + continue; + } + + const EwsItem &ewsItem = resp.item(); + KMime::Message::Ptr msg(new KMime::Message); + + // Rebuild the message headers + QVariant v = ewsItem[EwsItemFieldSubject]; + if (Q_LIKELY(v.isValid())) { + msg->subject()->fromUnicodeString(v.toString(), "utf-8"); + } + + v = ewsItem[EwsItemFieldFrom]; + if (Q_LIKELY(v.isValid())) { + EwsMailbox mbox = v.value(); + msg->from()->addAddress(mbox); + } + + v = ewsItem[EwsItemFieldToRecipients]; + if (Q_LIKELY(v.isValid())) { + EwsMailbox::List mboxList = v.value(); + QStringList addrList; + Q_FOREACH (const EwsMailbox &mbox, mboxList) { + msg->to()->addAddress(mbox); + } + } + + v = ewsItem[EwsItemFieldCcRecipients]; + if (Q_LIKELY(v.isValid())) { + EwsMailbox::List mboxList = v.value(); + QStringList addrList; + Q_FOREACH (const EwsMailbox &mbox, mboxList) { + msg->cc()->addAddress(mbox); + } + } + + v = ewsItem[EwsItemFieldBccRecipients]; + if (v.isValid()) { + EwsMailbox::List mboxList = v.value(); + QStringList addrList; + Q_FOREACH (const EwsMailbox &mbox, mboxList) { + msg->bcc()->addAddress(mbox); + } + } + + v = ewsItem[EwsItemFieldInternetMessageId]; + if (v.isValid()) { + msg->messageID()->from7BitString(v.toString().toAscii()); + } + + v = ewsItem[EwsItemFieldInReplyTo]; + if (v.isValid()) { + msg->inReplyTo()->from7BitString(v.toString().toAscii()); + } + + v = ewsItem[EwsItemFieldDateTimeReceived]; + if (v.isValid()) { + msg->date()->setDateTime(v.toDateTime()); + } + + v = ewsItem[EwsItemFieldReferences]; + if (v.isValid()) { + msg->references()->from7BitString(v.toString().toAscii()); + } + + v = ewsItem[EwsItemFieldReplyTo]; + if (v.isValid()) { + EwsMailbox mbox = v.value(); + msg->replyTo()->addAddress(mbox); + } + + msg->assemble(); + item.setPayload(KMime::Message::Ptr(msg)); + + v = ewsItem[EwsItemFieldSize]; + if (v.isValid()) { + item.setSize(v.toUInt()); + } + + // Workaround for Akonadi bug + // When setting flags, adding each of them separately vs. setting a list in one go makes + // a difference. In the former case the item treats this as an incremental change and + // records flags added and removed. In the latter it sets a flag indicating that flags were + // reset. + // For some strange reason Akonadi is not seeing the flags in the latter case. + Q_FOREACH (const QByteArray &flag, EwsMailHandler::readFlags(ewsItem)) { + item.setFlag(flag); + } + qCDebugNC(EWSRES_LOG) << "EwsFetchMailDetailJob::processItems:" << ewsHash(item.remoteId()) << item.flags(); + + it++; + } + + qCDebugNC(EWSRES_LOG) << "EwsFetchMailDetailJob::processItems: done"; + + emitResult(); +} diff --git a/resources/ews/mail/ewsmailhandler.h b/resources/ews/mail/ewsmailhandler.h new file mode 100644 --- /dev/null +++ b/resources/ews/mail/ewsmailhandler.h @@ -0,0 +1,48 @@ +/* + 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. +*/ + +#ifndef EWSMAILHANDLER_H +#define EWSMAILHANDLER_H + +#include "ewsitemhandler.h" + +class EwsMailHandler : public EwsItemHandler +{ +public: + EwsMailHandler(); + virtual ~EwsMailHandler(); + + virtual EwsFetchItemDetailJob *fetchItemDetailJob(EwsClient &client, QObject *parent, + const Akonadi::Collection &collection) override; + virtual void setSeenFlag(Akonadi::Item &item, bool value) override; + virtual QString mimeType() override; + virtual bool setItemPayload(Akonadi::Item &item, const EwsItem &ewsItem) override; + virtual EwsModifyItemJob *modifyItemJob(EwsClient &client, const QVector &items, + const QSet &parts, QObject *parent) override; + virtual EwsCreateItemJob *createItemJob(EwsClient &client, const Akonadi::Item &item, + const Akonadi::Collection &collection, + EwsTagStore *tagStore, EwsResource *parent) override; + static EwsItemHandler *factory(); + static QHash writeFlags(const QSet flags); + static QSet readFlags(const EwsItem &item); + static QList flagsProperties(); +private: +}; + +#endif diff --git a/resources/ews/mail/ewsmailhandler.cpp b/resources/ews/mail/ewsmailhandler.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/mail/ewsmailhandler.cpp @@ -0,0 +1,181 @@ +/* + 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 "ewsmailhandler.h" + +#include +#include +#include + +#include "ewsfetchmaildetailjob.h" +#include "ewsmodifymailjob.h" +#include "ewscreatemailjob.h" + +using namespace Akonadi; + +static const EwsPropertyField propPidFlagStatus(0x1090, EwsPropTypeInteger); + +EwsMailHandler::EwsMailHandler() +{ +} + +EwsMailHandler::~EwsMailHandler() +{ +} + +EwsItemHandler *EwsMailHandler::factory() +{ + return new EwsMailHandler(); +} + +EwsFetchItemDetailJob *EwsMailHandler::fetchItemDetailJob(EwsClient &client, QObject *parent, + const Akonadi::Collection &collection) +{ + return new EwsFetchMailDetailJob(client, parent, collection); +} + +void EwsMailHandler::setSeenFlag(Item &item, bool value) +{ + if (value) { + item.setFlag(MessageFlags::Seen); + } else { + item.clearFlag(MessageFlags::Seen); + } +} + +QString EwsMailHandler::mimeType() +{ + return KMime::Message::mimeType(); +} + +bool EwsMailHandler::setItemPayload(Akonadi::Item &item, const EwsItem &ewsItem) +{ + QByteArray mimeContent = ewsItem[EwsItemFieldMimeContent].toByteArray(); + if (mimeContent.isEmpty()) { + qWarning() << QStringLiteral("MIME content is empty!"); + return false; + } + + mimeContent.replace("\r\n", "\n"); + + KMime::Message::Ptr msg(new KMime::Message); + msg->setContent(mimeContent); + msg->parse(); + // Some messages might just be empty (just headers). This results in the body being empty. + // The problem is that when Akonadi sees an empty body it will interpret this as "body not + // yet loaded" and will retry which will cause an endless loop. To work around this put a + // single newline so that it is not empty. + if (msg->body().isEmpty()) { + msg->setBody("\n"); + } + Q_FOREACH (KMime::Content* c, msg->attachments()) { + KMime::Headers::ContentID *cid = c->contentID(false); + } + item.setPayload(msg); + return true; +} + +EwsModifyItemJob *EwsMailHandler::modifyItemJob(EwsClient &client, const QVector &items, + const QSet &parts, QObject *parent) +{ + return new EwsModifyMailJob(client, items, parts, parent); +} + +EwsCreateItemJob *EwsMailHandler::createItemJob(EwsClient &client, const Akonadi::Item &item, + const Akonadi::Collection &collection, + EwsTagStore *tagStore, EwsResource *parent) +{ + return new EwsCreateMailJob(client, item, collection, tagStore, parent); +} + +QHash EwsMailHandler::writeFlags(const QSet flags) +{ + // Strip all the message flags that can be stored in dedicated Exchange fields and leave + // any remaining ones to be stored in a private property. + + QSet unknownFlags; + QHash propertyHash; + bool isRead = false; + bool isFlagged = false; + + Q_FOREACH (const QByteArray &flag, flags) { + if (flag == MessageFlags::Seen) { + isRead = true; + } else if (flag == MessageFlags::Flagged) { + isFlagged = true; + } else if (flag == MessageFlags::HasAttachment || flag == MessageFlags::HasInvitation || + flag == MessageFlags::Signed || flag == MessageFlags::Encrypted) { + // These flags are read-only. Remove them from the unknown list but don't do anything with them. + } else { + unknownFlags.insert(flag); + } + } + + propertyHash.insert(EwsPropertyField(QStringLiteral("message:IsRead")), + isRead ? QStringLiteral("true") : QStringLiteral("false")); + if (isFlagged) { + propertyHash.insert(propPidFlagStatus, QStringLiteral("2")); + } else { + propertyHash.insert(propPidFlagStatus, QVariant()); + } + + propertyHash.unite(EwsItemHandler::writeFlags(unknownFlags)); + + return propertyHash; +} + +QSet EwsMailHandler::readFlags(const EwsItem &item) +{ + QSet flags = EwsItemHandler::readFlags(item); + + QVariant v = item[EwsItemFieldIsRead]; + if (v.isValid() && v.toBool()) { + flags.insert(MessageFlags::Seen); + } + + v = item[EwsItemFieldHasAttachments]; + if (v.isValid() && v.toBool()) { + flags.insert(MessageFlags::HasAttachment); + } + + QVariant flagProp = item[propPidFlagStatus]; + if (!flagProp.isNull() && (flagProp.toUInt() == 2)) { + flags.insert(MessageFlags::Flagged); + } + + if (item.type() == EwsItemTypeMeetingRequest) { + flags.insert(MessageFlags::HasInvitation); + } + + return flags; +} + +QList EwsMailHandler::flagsProperties() +{ + QList props = EwsItemHandler::flagsProperties(); + + props.append(propPidFlagStatus); + props.append(EwsPropertyField(QStringLiteral("message:IsRead"))); + props.append(EwsPropertyField(QStringLiteral("item:HasAttachments"))); + + return props; +} + +EWS_DECLARE_ITEM_HANDLER(EwsMailHandler, EwsItemTypeMessage) +EWS_DECLARE_ITEM_HANDLER(EwsMailHandler, EwsItemTypePostItem) diff --git a/resources/ews/mail/ewsmodifymailjob.h b/resources/ews/mail/ewsmodifymailjob.h new file mode 100644 --- /dev/null +++ b/resources/ews/mail/ewsmodifymailjob.h @@ -0,0 +1,37 @@ +/* + 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. +*/ + +#ifndef EWSMODIFYMAILJOB_H +#define EWSMODIFYMAILJOB_H + +#include "ewsmodifyitemjob.h" + +class EwsModifyMailJob : public EwsModifyItemJob +{ + Q_OBJECT +public: + EwsModifyMailJob(EwsClient &client, const Akonadi::Item::List &items, const QSet &parts, + QObject *parent); + virtual ~EwsModifyMailJob(); + virtual void start() override; +private Q_SLOTS: + void updateItemFinished(KJob *job); +}; + +#endif diff --git a/resources/ews/mail/ewsmodifymailjob.cpp b/resources/ews/mail/ewsmodifymailjob.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/mail/ewsmodifymailjob.cpp @@ -0,0 +1,106 @@ +/* + 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 "ewsmodifymailjob.h" + +#include + +#include "ewsupdateitemrequest.h" +#include "ewsmailhandler.h" + +#include "ewsresource_debug.h" + +using namespace Akonadi; + +EwsModifyMailJob::EwsModifyMailJob(EwsClient &client, const Akonadi::Item::List &items, + const QSet &parts, QObject *parent) + : EwsModifyItemJob(client, items, parts, parent) +{ +} +EwsModifyMailJob::~EwsModifyMailJob() +{ +} + +void EwsModifyMailJob::start() +{ + bool doSubmit = false; + EwsUpdateItemRequest *req = new EwsUpdateItemRequest(mClient, this); + EwsId itemId; + + Q_FOREACH (const Item &item, mItems) { + itemId = EwsId(item.remoteId(), item.remoteRevision()); + + if (mParts.contains("FLAGS")) { + EwsUpdateItemRequest::ItemChange ic(itemId, EwsItemTypeMessage); + QHash propertyHash = EwsMailHandler::writeFlags(item.flags()); + + for (auto it = propertyHash.cbegin(); it != propertyHash.cend(); ++it) { + EwsUpdateItemRequest::Update *upd; + if (it.value().isNull()) { + upd = new EwsUpdateItemRequest::DeleteUpdate(it.key()); + } else { + upd = new EwsUpdateItemRequest::SetUpdate(it.key(), it.value()); + } + ic.addUpdate(upd); + } + + req->addItemChange(ic); + doSubmit = true; + } + } + + if (doSubmit) { + connect(req, SIGNAL(result(KJob*)), SLOT(updateItemFinished(KJob*))); + req->start(); + } else { + delete req; + emitResult(); + } +} + +void EwsModifyMailJob::updateItemFinished(KJob *job) +{ + if (job->error()) { + setErrorText(job->errorString()); + emitResult(); + return; + } + + EwsUpdateItemRequest *req = qobject_cast(job); + if (!req) { + setErrorText(QStringLiteral("Invalid EwsUpdateItemRequest job object")); + emitResult(); + return; + } + + Q_ASSERT(req->responses().size() == mItems.size()); + + Item::List::iterator it = mItems.begin(); + Q_FOREACH (const EwsUpdateItemRequest::Response &resp, req->responses()) { + if (!resp.isSuccess()) { + setErrorText(QStringLiteral("Item update failed: ") + resp.responseMessage()); + emitResult(); + return; + } + + it->setRemoteRevision(resp.itemId().changeKey()); + } + + emitResult(); +} diff --git a/resources/ews/mtaconfigdialog.h b/resources/ews/mtaconfigdialog.h new file mode 100644 --- /dev/null +++ b/resources/ews/mtaconfigdialog.h @@ -0,0 +1,51 @@ +/* + 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 MTACONFIGDIALOG_H +#define MTACONFIGDIALOG_H + +#include + +class QDialogButtonBox; +namespace Akonadi +{ +class ManageAccountWidget; +} +namespace Ui +{ +class SetupServerView; +} +class EwsMtaResource; + +class MtaConfigDialog : public QDialog +{ + Q_OBJECT +public: + explicit MtaConfigDialog(EwsMtaResource *parentResource, WId windowId); + virtual ~MtaConfigDialog(); +private Q_SLOTS: + void save(); + void dialogAccepted(); +private: + QDialogButtonBox *mButtonBox; + EwsMtaResource *mParentResource; + Ui::SetupServerView *mUi; +}; + +#endif diff --git a/resources/ews/mtaconfigdialog.cpp b/resources/ews/mtaconfigdialog.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/mtaconfigdialog.cpp @@ -0,0 +1,93 @@ +/* + Copyright (C) 2015-2016 Krzysztof Nowicki + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "mtaconfigdialog.h" + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include "ewsmtaresource.h" +#include "mtasettings.h" +#include "ui_mtaconfigdialog.h" + +MtaConfigDialog::MtaConfigDialog(EwsMtaResource *parentResource, WId wId) + : QDialog(), mParentResource(parentResource) +{ + if (wId) { + KWindowSystem::setMainWindow(this, wId); + } + + mButtonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + QWidget *mainWidget = new QWidget(this); + QVBoxLayout *mainLayout = new QVBoxLayout(); + setLayout(mainLayout); + mainLayout->addWidget(mainWidget); + QPushButton *okButton = mButtonBox->button(QDialogButtonBox::Ok); + okButton->setDefault(true); + okButton->setShortcut(Qt::CTRL | Qt::Key_Return); + connect(mButtonBox, &QDialogButtonBox::accepted, this, &MtaConfigDialog::accept); + connect(mButtonBox, &QDialogButtonBox::rejected, this, &MtaConfigDialog::reject); + mainLayout->addWidget(mButtonBox); + + setWindowTitle(i18n("Microsoft Exchange Mail Transport Configuration")); + + mUi = new Ui::SetupServerView; + mUi->setupUi(mainWidget); + mUi->accountName->setText(parentResource->name()); + + Akonadi::AgentFilterProxyModel *model = mUi->resourceWidget->agentFilterProxyModel(); + model->addCapabilityFilter(QStringLiteral("X-EwsMailTransport")); + mUi->resourceWidget->view()->setSelectionMode(QAbstractItemView::SingleSelection); + + for (int i = 0; i < model->rowCount(); i++) { + QModelIndex index = model->index(i, 0); + QVariant v = model->data(index, Akonadi::AgentInstanceModel::InstanceIdentifierRole); + if (v.toString() == MtaSettings::ewsResource()) { + mUi->resourceWidget->view()->setCurrentIndex(index); + } + } + + connect(okButton, &QPushButton::clicked, this, &MtaConfigDialog::save); +} + +MtaConfigDialog::~MtaConfigDialog() +{ + delete mUi; +} + +void MtaConfigDialog::save() +{ + MtaSettings::setEwsResource(mUi->resourceWidget->selectedAgentInstances().first().identifier()); + mParentResource->setName(mUi->accountName->text()); + MtaSettings::self()->save(); +} + +void MtaConfigDialog::dialogAccepted() +{ +} + diff --git a/resources/ews/mtaconfigdialog.ui b/resources/ews/mtaconfigdialog.ui new file mode 100644 --- /dev/null +++ b/resources/ews/mtaconfigdialog.ui @@ -0,0 +1,55 @@ + + + SetupServerView + + + + 0 + 0 + 553 + 421 + + + + EWS Mail Transport Configuration + + + + + + Account Name: + + + + + + + + + + Parent Exchange account: + + + + + + + + 0 + 0 + + + + + + + + + Akonadi::AgentInstanceWidget + QWidget +
agentinstancewidget.h
+
+
+ + +
diff --git a/resources/ews/mtasettings.kcfgc b/resources/ews/mtasettings.kcfgc new file mode 100644 --- /dev/null +++ b/resources/ews/mtasettings.kcfgc @@ -0,0 +1,7 @@ +File=ewsmtaresource.kcfg +ClassName=MtaSettings +Mutators=true +ItemAccessors=true +SetUserTexts=true +Singleton=true +GlobalEnums=true diff --git a/resources/ews/progressdialog.h b/resources/ews/progressdialog.h new file mode 100644 --- /dev/null +++ b/resources/ews/progressdialog.h @@ -0,0 +1,40 @@ +/* + 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 PROGRESSDIALOG_H +#define PROGRESSDIALOG_H + +#include + +class KJob; + +class ProgressDialog : public QDialog +{ + Q_OBJECT +public: + enum Type { + AutoDiscovery, + TryConnect + }; + + ProgressDialog(QWidget *parent, Type type); + virtual ~ProgressDialog(); +}; + +#endif diff --git a/resources/ews/progressdialog.cpp b/resources/ews/progressdialog.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/progressdialog.cpp @@ -0,0 +1,76 @@ +/* + 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 "progressdialog.h" + +#include +#include +#include +#include +#include + +#include + +ProgressDialog::ProgressDialog(QWidget *parent, Type type) + : QDialog(parent) +{ + setModal(true); + QLabel *statusLabel = new QLabel(this); + + QProgressBar *progress = new QProgressBar(this); + progress->setMaximum(0); + + QVBoxLayout *vLayout = new QVBoxLayout(this); + + vLayout->setMargin(0); + + vLayout->addWidget(statusLabel); + vLayout->addWidget(progress); + + QPushButton *cancelButton = new QPushButton(this); + cancelButton->setText(i18n("Cancel")); + + QWidget *progressContainer = new QWidget(this); + progressContainer->setLayout(vLayout); + + QHBoxLayout *hLayout = new QHBoxLayout(this); + + hLayout->addWidget(progressContainer); + hLayout->addWidget(cancelButton); + + setLayout(hLayout); + hLayout->setSizeConstraint(QLayout::SetFixedSize); + + switch (type) { + case AutoDiscovery: + setWindowTitle(i18n("Exchange server autodiscovery")); + statusLabel->setText(i18n("Performing Microsoft Exchange server autodiscovery...")); + break; + case TryConnect: + setWindowTitle(i18n("Connecting to Exchange")); + statusLabel->setText(i18n("Connecting to Microsoft Exchange server...")); + break; + } + + connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject); +} + +ProgressDialog::~ProgressDialog() +{ +} diff --git a/resources/ews/settings.h b/resources/ews/settings.h new file mode 100644 --- /dev/null +++ b/resources/ews/settings.h @@ -0,0 +1,44 @@ +/* + 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 SETTINGS_H +#define SETTINGS_H + +#include "settingsbase.h" + +class Settings : public SettingsBase +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.Akonadi.Ews.Wallet") +public: + explicit Settings(WId windowId); + virtual ~Settings(); + + bool requestPassword(QString &password, bool ask); +public Q_SLOTS: + Q_SCRIPTABLE void setPassword(const QString &password); + Q_SCRIPTABLE void setTestPassword(const QString &password); + +private: + WId mWindowId; + QString mPassword; +}; + +#endif + diff --git a/resources/ews/settings.cpp b/resources/ews/settings.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/settings.cpp @@ -0,0 +1,100 @@ +/* + 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 "settings.h" + +#include + +#include +#include +#include + +Settings::Settings(WId windowId) + : SettingsBase(), mWindowId(windowId) +{ +} + +Settings::~Settings() +{ +} + +bool Settings::requestPassword(QString &password, bool ask) +{ + bool status = true; + + if (!mPassword.isEmpty()) { + password = mPassword; + return true; + } + + QScopedPointer wallet(KWallet::Wallet::openWallet(KWallet::Wallet::NetworkWallet(), + mWindowId)); + if (wallet && wallet->isOpen()) { + if (wallet->hasFolder(QStringLiteral("akonadi-ews"))) { + wallet->setFolder(QStringLiteral("akonadi-ews")); + wallet->readPassword(config()->name(), password); + } else { + wallet->createFolder(QStringLiteral("akonadi-ews")); + } + } else { + status = false; + } + + if (!status) { + if (!ask) { + return false; + } + + QPointer dlg = new KPasswordDialog(nullptr); + dlg->setModal(true); + dlg->setPrompt(i18n("Please enter password for user '%1' and Exchange account '%2'.", + username(), email())); + dlg->setAttribute(Qt::WA_DeleteOnClose); + if (dlg->exec() == QDialog::Accepted) { + password = dlg->password(); + setPassword(password); + } else { + delete dlg; + return false; + } + delete dlg; + } + + return true; +} + +void Settings::setPassword(const QString &password) +{ + mPassword = password; + QScopedPointer wallet(KWallet::Wallet::openWallet(KWallet::Wallet::NetworkWallet(), + mWindowId)); + if (wallet && wallet->isOpen()) { + if (!wallet->hasFolder(QStringLiteral("akonadi-ews"))) { + wallet->createFolder(QStringLiteral("akonadi-ews")); + } + wallet->setFolder(QStringLiteral("akonadi-ews")); + wallet->writePassword(config()->name(), password); + } +} + +void Settings::setTestPassword(const QString &password) +{ + mPassword = password; +} + diff --git a/resources/ews/settingsbase.kcfgc b/resources/ews/settingsbase.kcfgc new file mode 100644 --- /dev/null +++ b/resources/ews/settingsbase.kcfgc @@ -0,0 +1,7 @@ +File=ewsresource.kcfg +ClassName=SettingsBase +Mutators=true +ItemAccessors=true +SetUserTexts=true +Singleton=false +GlobalEnums=true diff --git a/resources/ews/tags/ewsakonaditagssyncjob.h b/resources/ews/tags/ewsakonaditagssyncjob.h new file mode 100644 --- /dev/null +++ b/resources/ews/tags/ewsakonaditagssyncjob.h @@ -0,0 +1,51 @@ +/* + 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. +*/ + +#ifndef EWSAKONADITAGSSYNCJOB_H +#define EWSAKONADITAGSSYNCJOB_H + +#include "ewsjob.h" + +class EwsTagStore; +class EwsClient; +namespace Akonadi +{ +class Collection; +} + +class EwsAkonadiTagsSyncJob : public EwsJob +{ + Q_OBJECT +public: + EwsAkonadiTagsSyncJob(EwsTagStore *tagStore, EwsClient &client, + const Akonadi::Collection &rootCollection, QObject *parent); + ~EwsAkonadiTagsSyncJob(); + + void start() override; + +private Q_SLOTS: + void tagFetchFinished(KJob *job); + void tagWriteFinished(KJob *job); +private: + EwsTagStore *mTagStore; + EwsClient &mClient; + const Akonadi::Collection &mRootCollection; +}; + +#endif diff --git a/resources/ews/tags/ewsakonaditagssyncjob.cpp b/resources/ews/tags/ewsakonaditagssyncjob.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/tags/ewsakonaditagssyncjob.cpp @@ -0,0 +1,70 @@ +/* + 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 "ewsakonaditagssyncjob.h" + +#include +#include "ewstagstore.h" +#include "ewsglobaltagswritejob.h" + +using namespace Akonadi; + +EwsAkonadiTagsSyncJob::EwsAkonadiTagsSyncJob(EwsTagStore *tagStore, EwsClient &client, + const Collection &rootCollection, QObject *parent) + : EwsJob(parent), mTagStore(tagStore), mClient(client), mRootCollection(rootCollection) +{ +} + +EwsAkonadiTagsSyncJob::~EwsAkonadiTagsSyncJob() +{ +} + +void EwsAkonadiTagsSyncJob::start() +{ + TagFetchJob *job = new TagFetchJob(this); + connect(job, &TagFetchJob::result, this, &EwsAkonadiTagsSyncJob::tagFetchFinished); +} + +void EwsAkonadiTagsSyncJob::tagFetchFinished(KJob *job) +{ + if (job->error()) { + setErrorMsg(job->errorString()); + emitResult(); + return; + } + + TagFetchJob *tagJob = qobject_cast(job); + Q_ASSERT(tagJob); + + if (mTagStore->syncTags(tagJob->tags())) { + EwsGlobalTagsWriteJob *tagJob = new EwsGlobalTagsWriteJob(mTagStore, mClient, mRootCollection, this); + connect(tagJob, &EwsGlobalTagsWriteJob::result, this, &EwsAkonadiTagsSyncJob::tagWriteFinished); + tagJob->start(); + } else { + emitResult(); + } +} +void EwsAkonadiTagsSyncJob::tagWriteFinished(KJob *job) +{ + if (job->error()) { + setErrorMsg(job->errorString()); + } + + emitResult(); +} diff --git a/resources/ews/tags/ewsglobaltagsreadjob.h b/resources/ews/tags/ewsglobaltagsreadjob.h new file mode 100644 --- /dev/null +++ b/resources/ews/tags/ewsglobaltagsreadjob.h @@ -0,0 +1,57 @@ +/* + 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. +*/ + +#ifndef EWSGLOBALTAGSREADJOB_H +#define EWSGLOBALTAGSREADJOB_H + +#include "ewsjob.h" + +#include + +class EwsTagStore; +class EwsClient; +namespace Akonadi +{ +class Collection; +} + +class EwsGlobalTagsReadJob : public EwsJob +{ + Q_OBJECT +public: + EwsGlobalTagsReadJob(EwsTagStore *tagStore, EwsClient &client, + const Akonadi::Collection &rootCollection, QObject *parent); + ~EwsGlobalTagsReadJob(); + + void start() override; + + const Akonadi::Tag::List &tags() const + { + return mTags; + }; +private Q_SLOTS: + void getFolderRequestFinished(KJob *job); +private: + EwsTagStore *mTagStore; + EwsClient &mClient; + const Akonadi::Collection &mRootCollection; + Akonadi::Tag::List mTags; +}; + +#endif diff --git a/resources/ews/tags/ewsglobaltagsreadjob.cpp b/resources/ews/tags/ewsglobaltagsreadjob.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/tags/ewsglobaltagsreadjob.cpp @@ -0,0 +1,88 @@ +/* + 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 "ewsglobaltagsreadjob.h" + +#include "ewsclient.h" +#include "ewsgetfolderrequest.h" +#include "ewstagstore.h" +#include "ewsresource.h" + +#include "ewsresource_debug.h" + +using namespace Akonadi; + +EwsGlobalTagsReadJob::EwsGlobalTagsReadJob(EwsTagStore *tagStore, EwsClient &client, + const Collection &rootCollection, QObject *parent) + : EwsJob(parent), mTagStore(tagStore), mClient(client), mRootCollection(rootCollection) +{ +} + +EwsGlobalTagsReadJob::~EwsGlobalTagsReadJob() +{ +} + +void EwsGlobalTagsReadJob::start() +{ + EwsGetFolderRequest *req = new EwsGetFolderRequest(mClient, this); + req->setFolderIds(EwsId::List() << EwsId(EwsDIdMsgFolderRoot)); + EwsFolderShape shape(EwsShapeIdOnly); + shape << EwsResource::globalTagsProperty << EwsResource::globalTagsVersionProperty; + req->setFolderShape(shape); + connect(req, &EwsGetFolderRequest::result, this, &EwsGlobalTagsReadJob::getFolderRequestFinished); + req->start(); +} + +void EwsGlobalTagsReadJob::getFolderRequestFinished(KJob *job) +{ + EwsGetFolderRequest *req = qobject_cast(job); + + if (!req) { + qCWarning(EWSRES_LOG) << QStringLiteral("Invalid EwsGetFolderRequest job object"); + setErrorMsg(QStringLiteral("Invalid EwsGetFolderRequest job object")); + emitResult(); + return; + } + + if (req->error()) { + setErrorMsg(req->errorString()); + emitResult(); + return; + } + + if (req->responses().size() != 1) { + qCWarning(EWSRES_LOG) << QStringLiteral("Invalid number of responses received"); + setErrorMsg(QStringLiteral("Invalid number of responses received")); + emitResult(); + return; + } + + EwsFolder folder = req->responses().first().folder(); + bool status = mTagStore->readTags(folder[EwsResource::globalTagsProperty].toStringList(), + folder[EwsResource::globalTagsVersionProperty].toInt()); + if (!status) { + qCWarning(EWSRES_LOG) << QStringLiteral("Incorrect server tag data"); + setErrorMsg(QStringLiteral("Incorrect server tag data")); + emitResult(); + } + + mTags = mTagStore->tags(); + + emitResult(); +} diff --git a/resources/ews/tags/ewsglobaltagswritejob.h b/resources/ews/tags/ewsglobaltagswritejob.h new file mode 100644 --- /dev/null +++ b/resources/ews/tags/ewsglobaltagswritejob.h @@ -0,0 +1,49 @@ +/* + 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. +*/ + +#ifndef EWSGLOBALTAGSWRITEJOB_H +#define EWSGLOBALTAGSWRITEJOB_H + +#include "ewsjob.h" + +class EwsTagStore; +class EwsClient; +namespace Akonadi +{ +class Collection; +} + +class EwsGlobalTagsWriteJob : public EwsJob +{ + Q_OBJECT +public: + EwsGlobalTagsWriteJob(EwsTagStore *tagStore, EwsClient &client, + const Akonadi::Collection &rootCollection, QObject *parent); + ~EwsGlobalTagsWriteJob(); + + void start() override; +private Q_SLOTS: + void updateFolderRequestFinished(KJob *job); +private: + EwsTagStore *mTagStore; + EwsClient &mClient; + const Akonadi::Collection &mRootCollection; +}; + +#endif diff --git a/resources/ews/tags/ewsglobaltagswritejob.cpp b/resources/ews/tags/ewsglobaltagswritejob.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/tags/ewsglobaltagswritejob.cpp @@ -0,0 +1,67 @@ +/* + 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 "ewsglobaltagswritejob.h" + +#include + +#include "ewsupdatefolderrequest.h" +#include "ewsid.h" +#include "ewsitemhandler.h" +#include "ewstagstore.h" +#include "ewsresource.h" + +using namespace Akonadi; + +EwsGlobalTagsWriteJob::EwsGlobalTagsWriteJob(EwsTagStore *tagStore, EwsClient &client, + const Collection &rootCollection, QObject *parent) + : EwsJob(parent), mTagStore(tagStore), mClient(client), mRootCollection(rootCollection) +{ +} + +EwsGlobalTagsWriteJob::~EwsGlobalTagsWriteJob() +{ +} + +void EwsGlobalTagsWriteJob::start() +{ + QStringList tagList = mTagStore->serialize(); + + EwsUpdateFolderRequest *req = new EwsUpdateFolderRequest(mClient, this); + EwsUpdateFolderRequest::FolderChange fc(EwsId(mRootCollection.remoteId(), mRootCollection.remoteRevision()), + EwsFolderTypeMail); + EwsUpdateFolderRequest::Update *upd + = new EwsUpdateFolderRequest::SetUpdate(EwsResource::globalTagsProperty, tagList); + fc.addUpdate(upd); + upd = new EwsUpdateFolderRequest::SetUpdate(EwsResource::globalTagsVersionProperty, + QString::number(mTagStore->version())); + fc.addUpdate(upd); + req->addFolderChange(fc); + connect(req, &EwsUpdateFolderRequest::result, this, &EwsGlobalTagsWriteJob::updateFolderRequestFinished); + req->start(); +} + +void EwsGlobalTagsWriteJob::updateFolderRequestFinished(KJob *job) +{ + if (job->error()) { + setErrorMsg(job->errorText()); + } + + emitResult(); +} diff --git a/resources/ews/tags/ewstagstore.h b/resources/ews/tags/ewstagstore.h new file mode 100644 --- /dev/null +++ b/resources/ews/tags/ewstagstore.h @@ -0,0 +1,226 @@ +/* + 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. +*/ + +#ifndef EWSTAGSTORE_H +#define EWSTAGSTORE_H + +#include +#include + +#include "ewspropertyfield.h" + +class EwsItem; + +/** + * @brief EWS Tag storage helper class + * + * The role of this class is to maintain a list of tags within an EWS resource and synchronize + * the tags between Akonadi items and the Exchange server. + * + * Exchange tag support is rather basic. Tags are not first-class objects like folders or items, + * but exist as 'categories' assigned to items. Categories are string labels attached to an item. + * Each item can have any number of categories. + * + * Outlook (and OWA) are able to assign colors to tags. This is done outside of Exchange in form + * of extra metadata stored in a hidden item in the default Calendar folder. This metadata is + * binary and private to Outlook (although some documentation exists about it). + * + * On the Akonadi side tags are first-class citizens and can carry much more information than + * Exchange or Outlook private metadata is able to store. In order to store Akonadi tags with all + * their content the EWS resource makes use of private properties. + * + * @section tags_global_list Global tag list + * + * Similar to Outlook the Akonadi EWS resource keeps a master list of tags. The list is stored + * in a private property in the root mailbox folder. This list contains a list of tags serialized + * into Base64 strings. + * + * This list is updated each time tags are added, changed or deleted. It is also retrieved during + * startup in order to populate the Akonadi database with tags potentially stored by another + * instance of the resource on another machine. + * + * Along with the tag list a version field is stored in form of an integer. Each time the tag list + * is updated the version is incremented by one. This field allows another resource instance running + * in parallel to detect that the global tag list has changed and synchronize it with Akonadi. + * + * There are no attempts made to synchronize Akonadi and Outlook tag lists. + * + * @section tags_item_list Item tag list + * + * Tags for items are also stored in a private property. The list contains an array of tag unique + * identifiers. In parallel to this any update of item tag also populates the category list with + * the tag names. This provides a one-way synchronization of tags from Akonadi to Outlook. + * + * @section tags_akonadi_sync Synchronization with Akonadi tags + * + * In the current version of Akonadi tag support is partially broken. One of the problematic issues + * is that Akonadi doesn't always tell the resource about the tags it has. This means that the list + * of tags can be out of sync with the list on Akonadi side. This causes two problems. One is that + * items from Akonadi can have tags that the resource has never seen before. Such tags will need + * to be fetched with their full content and written to the server before they can be attached to + * the item. Another problem is in the reverse order - an EWS item can contain tags for which the + * resource doesn't know the Akonadi tag ID. In order to add such a tag to the Akonadi item the + * resource needs to fetch the tags in order to learn their ID. + * + * The EwsAkonadiTagSyncJob job can be used to force a synchronization of Akonadi tags to Exchange. + * + * In future once Akonadi tag support is fixed the explicit synchronization will become obsolete. + */ +class EwsTagStore : public QObject +{ + Q_OBJECT +public: + explicit EwsTagStore(QObject *parent); + virtual ~EwsTagStore(); + + /** + * @brief Load the tag information from the supplied server-side list. + * + * Convenience method used to explicitly pass the server-side tag list to the store. This can + * be used if the tag information can be retrieved as part of some other operation. This saves + * an extra request to the server that would be needed when retrieveGlobalTags() is called. + */ + bool readTags(const QStringList &taglist, int version); + + /** + * @brief Writes tag-related properties to the EWS item + * + * This method can be used when an Akonadi item is about to be created/updated on the server. + * It will take the list of tags and fill the necessary Exchange properties used to store the + * tag information on the server. + */ + bool writeEwsProperties(const Akonadi::Item &item, EwsItem &ewsItem) const; + + /** + * @brief Reads tag information from the EWS item + * + * This method reads the server-side tag information for an item and populates the Akonadi + * item with tags. + * + * The code returns @e true if all tags have been converted from Exchange properties into + * Akonadi tags. If at least one tag is not found in Akonadi database the method returns + * @e false, unless @e ignoreMissing is set to true, in which case the missing tags are + * ignored. + */ + bool readEwsProperties(Akonadi::Item &item, const EwsItem &ewsItem, bool ignoreMissing) const; + + /** + * @brief Checks if a given Akonadi tag is in the store + */ + bool containsId(Akonadi::Tag::Id id) const; + + /** + * @brief Retrieve the remote identifier of a tag + * + * This identifier can be used to populate the per-item Exchange tag list property. + */ + QByteArray tagRemoteId(Akonadi::Tag::Id id) const; + + /** + * @brief Retrieve the display name of the tag + * + * This name can be used to populate the Exchange item category list. + */ + QString tagName(Akonadi::Tag::Id id) const; + + /** + * @brief Add or update a tag in the store. + * + * This method makes the tag known to the tag store. The tag object needs to be populated + * with all information that should be stored in Exchange (uid, attributes). + * + * @note This method does not update the server-side copy of the tag list. This needs to + * be done explicitly by starting an EwsGlobalTagsWriteJob. + */ + void addTag(const Akonadi::Tag &tag); + + /** + * @brief Add or update a list of tags in the store. + * + * This method makes the tags known to the tag store. The tag objects needs to be populated + * with all information that should be stored in Exchange (uid, attributes). + * + * @note This method does not update the server-side copy of the tag list. This needs to + * be done explicitly by starting an EwsGlobalTagsWriteJob. + */ + void addTags(const Akonadi::Tag::List &tags); + + /** + * @brief Remove a tag from the store. + * + * This method removes the tag from the tag store. + * + * @note This method does not update the server-side copy of the tag list. This needs to + * be done explicitly by starting an EwsGlobalTagsWriteJob. + */ + void removeTag(const Akonadi::Tag &tag); + + /** + * @brief Retrieve the Akonadi tag list + * + * This method retrieves a list of tags as Akonadi tag objects. All objects are fully populated + * with any data stored on the server. + */ + Akonadi::Tag::List tags() const; + + Akonadi::Tag::Id tagIdForRid(const QByteArray &rid) const; + + /** + * @brief Synchronize the store with tag list from Akonadi + * + * This function is an extension to addTags() in that for each tag it checks whether it already + * exists in the store and whether it has changed. + * + * The return value indicates if the store was modified during the synchronization. If at least + * one new or changed tag was found the return value is TRUE. Otherwise the method returns FALSE. + * The return value serves as information as to whether the tags should be pushed to Exchange. + */ + bool syncTags(const Akonadi::Tag::List &tags); + + /** + * @brief Retrieve a list of serialized tags + * + * Returns a list of tags serialized into strings. This list is intended to be stored on the + * Exchange server. + */ + QStringList serialize() const; + + /** + * @brief Returns the current tag list version + * + * Each time a change is made to the tag list the version number is incremented. This number + * can later be used to determine if a tag list sync from Exchange is needed or not. + */ + int version() const; + +private: + QByteArray serializeTag(const Akonadi::Tag &tag) const; + bool unserializeTag(const QByteArray &data, Akonadi::Tag &tag) const; + + /** Master tag list (keyed by the tag remote identifier) */ + QHash mTagData; + /** Mapping used to determine the tag remote identifier based on Akonadi tag identifier. */ + QHash mTagIdMap; + /** Mapping used to determine the tag display name based on Akonadi tag identifier. */ + QHash mTagNameMap; + + int mVersion; +}; + +#endif diff --git a/resources/ews/tags/ewstagstore.cpp b/resources/ews/tags/ewstagstore.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/tags/ewstagstore.cpp @@ -0,0 +1,325 @@ +/* + 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 "ewstagstore.h" + +#include + +#include +#include + +#include "ewsresource_debug.h" +#include "ewsitem.h" +#include "ewsresource.h" + +using namespace Akonadi; + +static Q_CONSTEXPR quint32 TagDataVer1 = 1; +static Q_CONSTEXPR QDataStream::Version TagDataVer1StreamVer = QDataStream::Qt_5_4; + +EwsTagStore::EwsTagStore(QObject *parent) + : QObject(parent), mVersion(0) +{ + +} + +EwsTagStore::~EwsTagStore() +{ + +} + +QByteArray EwsTagStore::serializeTag(const Akonadi::Tag &tag) const +{ + QByteArray tagData; + QDataStream stream(&tagData, QIODevice::WriteOnly); + + stream.setVersion(TagDataVer1StreamVer); + stream << TagDataVer1; + stream << tag.name() << tag.gid(); + Attribute::List attrs = tag.attributes(); + stream << (int)attrs.size(); + + Q_FOREACH (const Attribute *attr, attrs) { + stream << attr->type(); + stream << attr->serialized(); + } + + return tagData; +} + +bool EwsTagStore::unserializeTag(const QByteArray &data, Akonadi::Tag &tag) const +{ + QDataStream stream(data); + + quint32 ver; + stream >> ver; + + QString name; + QByteArray gid; + int numAttrs; + Attribute::List attributes; + + switch (ver) { + case TagDataVer1: + stream.setVersion(TagDataVer1StreamVer); + stream >> name >> gid; + stream >> numAttrs; + if (stream.status() != QDataStream::Ok) { + QStringLiteral("Error reading tag version 1"); + return false; + } + + for (int i = 0; i < numAttrs; ++i) { + QByteArray attrType, attrData; + stream >> attrType >> attrData; + if (stream.status() != QDataStream::Ok) { + QStringLiteral("Error reading tag version 1"); + return false; + } + Attribute *attr = AttributeFactory::createAttribute(attrType); + attr->deserialize(data); + attributes.append(attr); + } + break; + default: + qCWarningNC(EWSRES_LOG) << QStringLiteral("Unknown tag data version (%1)").arg(ver); + return false; + } + tag.setName(name); + tag.setGid(gid); + + Q_FOREACH (Attribute *attr, attributes) { + tag.addAttribute(attr); + } + + return true; +} + +bool EwsTagStore::readTags(const QStringList &taglist, int version) +{ + + if (version < mVersion) { + qCWarningNC(EWSRES_LOG) << QStringLiteral("Reading tags from older version (have %1, got %2)") + .arg(mVersion).arg(version); + return false; + } else if (version == mVersion) { + qCDebugNC(EWSRES_LOG) << QStringLiteral("Both tag lists in version %1 - not syncing").arg(version); + return true; + } + + mTagData.clear(); + + Q_FOREACH (const QString &tag, taglist) { + QByteArray tagdata = qUncompress(QByteArray::fromBase64(tag.toAscii())); + if (tagdata.isNull()) { + qCDebugNC(EWSRES_LOG) << QStringLiteral("Incorrect tag data"); + } else { + QDataStream stream(tagdata); + stream.setVersion(QDataStream::Qt_5_4); + QByteArray key, data; + stream >> key >> data; + if (stream.status() != QDataStream::Ok) { + qCDebugNC(EWSRES_LOG) << QStringLiteral("Incorrect tag entry"); + mTagData.clear(); + return false; + } else { + mTagData.insert(key, data); + } + } + } + + mVersion = version; + + return true; +} + +QStringList EwsTagStore::serialize() const +{ + QStringList tagList; + + for (auto it = mTagData.cbegin(); it != mTagData.cend(); ++it) { + QByteArray data; + QDataStream stream(&data, QIODevice::WriteOnly); + stream.setVersion(QDataStream::Qt_5_4); + stream << it.key(); + stream << it.value(); + tagList.append(QString::fromLatin1(qCompress(data, 9).toBase64())); + } + + return tagList; +} + +Tag::List EwsTagStore::tags() const +{ + Tag::List tagList; + + for (auto it = mTagData.cbegin(); it != mTagData.cend(); ++it) { + Tag tag(-1); + if (unserializeTag(it.value(), tag)) { + tagList.append(tag); + } + } + + return tagList; +} + +bool EwsTagStore::containsId(Akonadi::Tag::Id id) const +{ + return mTagIdMap.contains(id); +} + +void EwsTagStore::addTag(const Akonadi::Tag &tag) +{ + addTags(Akonadi::Tag::List() << tag); +} + +void EwsTagStore::addTags(const Akonadi::Tag::List &tags) +{ + syncTags(tags); +} + +bool EwsTagStore::syncTags(const Akonadi::Tag::List &tags) +{ + /* TODO: Remote tag support is partially broken in Akonadi. In particular it is not possible + * to associate a remote identifier with a tag as the database schema is broken. Therefore + * the EWS resource relies on the fact that the remote identifier is the same as the local uid + * of the tag and doesn't set the remote identifier on the tag object. This is possinle as + * tags don't exist as Exchange objects and are only stored in private properties that are in + * full control. */ + + bool changed = false; + + QList tagIds = mTagData.keys(); + Q_FOREACH (const Tag &tag, tags) { + QByteArray serialized = serializeTag(tag); + auto it = mTagData.find(tag.gid()); + /* First check if the tag exists or if it has been changed. Only once that is done + * check if the store knows the tag name and Akonadi id. The separation is necessary as + * the store might have the full list of tags from Exchange, but without Akonadi IDs. When + * a sync is done it may only yield those IDs without any of the tags changed. In such case + * the function should return false as no actual change has been made. + */ + if ((it == mTagData.end()) || (*it != serialized)) { + mTagData.insert(tag.gid(), serialized); + changed = true; + } + if (it != mTagData.end()) { + tagIds.removeOne(tag.gid()); + } + if (!mTagIdMap.contains(tag.id())) { + mTagIdMap.insert(tag.id(), tag.gid()); + QString name; + if (tag.hasAttribute()) { + name = tag.attribute()->displayName(); + } else { + name = tag.name(); + } + if (!name.isEmpty()) { + mTagNameMap.insert(tag.id(), tag.name()); + } + } + } + + Q_FOREACH (const QByteArray &tagId, tagIds) { + mTagData.remove(tagId); + } + + if (changed) { + ++mVersion; + } + + return changed; +} + +void EwsTagStore::removeTag(const Akonadi::Tag &tag) +{ + QByteArray rid = mTagIdMap.value(tag.id()); + mTagIdMap.remove(tag.id()); + mTagNameMap.remove(tag.id()); + mTagData.remove(rid); + + ++mVersion; +} + +QByteArray EwsTagStore::tagRemoteId(Akonadi::Tag::Id id) const +{ + return mTagIdMap.value(id); +} + +QString EwsTagStore::tagName(Akonadi::Tag::Id id) const +{ + return mTagNameMap.value(id); +} + +Tag::Id EwsTagStore::tagIdForRid(const QByteArray &rid) const +{ + return mTagIdMap.key(rid, -1); +} + +bool EwsTagStore::readEwsProperties(Akonadi::Item &item, const EwsItem &ewsItem, bool ignoreMissing) const +{ + QVariant tagProp = ewsItem[EwsResource::tagsProperty]; + if (tagProp.isValid() && tagProp.canConvert()) { + QStringList tagRids = tagProp.toStringList(); + Tag::List tags; + Q_FOREACH (const QString &tagRid, tagRids) { + Tag::Id tagId = tagIdForRid(tagRid.toAscii()); + if (tagId == -1) { + /* Tag not found. */ + qCDebug(EWSRES_LOG) << QStringLiteral("Found missing tag: %1").arg(tagRid); + if (ignoreMissing) { + continue; + } else { + return false; + } + } + Tag tag(tagId); + item.setTag(tag); + } + } + + return true; +} + +bool EwsTagStore::writeEwsProperties(const Akonadi::Item &item, EwsItem &ewsItem) const +{ + if (!item.tags().isEmpty()) { + QStringList tagList; + QStringList categoryList; + Q_FOREACH (const Tag &tag, item.tags()) { + if (!containsId(tag.id())) { + return false; + } + tagList.append(QString::fromLatin1(tagRemoteId(tag.id()))); + QString name = tagName(tag.id()); + if (!name.isEmpty()) { + categoryList.append(name); + } + } + ewsItem.setProperty(EwsResource::tagsProperty, tagList); + ewsItem.setField(EwsItemFieldCategories, categoryList); + } + + return true; +} + +int EwsTagStore::version() const +{ + return mVersion; +} diff --git a/resources/ews/tags/ewsupdateitemstagsjob.h b/resources/ews/tags/ewsupdateitemstagsjob.h new file mode 100644 --- /dev/null +++ b/resources/ews/tags/ewsupdateitemstagsjob.h @@ -0,0 +1,67 @@ +/* + 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. +*/ + +#ifndef EWSUPDATEITEMSTAGSJOB_H +#define EWSUPDATEITEMSTAGSJOB_H + +#include "ewsjob.h" +#include + +class EwsTagStore; +class EwsClient; +class EwsResource; + +/** + * @brief Job used to update Exchange items wit tag information from Akonadi + * + * This job cycles through all items and updates the Exchange database with tag information from + * the items. + * + * The job relies on the tag store to retrieve tag identifiers and names that can be stored in + * Exchange. Due to buggy tag implementation in Akonad it can happen that items contain tags not + * yet known to the EWS resource. In such case an additional tag fetch job is issues to fetch + * information about those tags so that they can be added to the tag store. + */ +class EwsUpdateItemsTagsJob : public EwsJob +{ + Q_OBJECT +public: + EwsUpdateItemsTagsJob(const Akonadi::Item::List &items, EwsTagStore *tagStore, EwsClient &client, + EwsResource *parent); + ~EwsUpdateItemsTagsJob(); + + void start() override; + + Akonadi::Item::List items() + { + return mItems; + }; +private Q_SLOTS: + void itemsTagsChangedTagsFetched(KJob *job); + void updateItemsTagsRequestFinished(KJob *job); + void globalTagsWriteFinished(KJob *job); +private: + void doUpdateItemsTags(); + + Akonadi::Item::List mItems; + EwsTagStore *mTagStore; + EwsClient &mClient; +}; + +#endif diff --git a/resources/ews/tags/ewsupdateitemstagsjob.cpp b/resources/ews/tags/ewsupdateitemstagsjob.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/tags/ewsupdateitemstagsjob.cpp @@ -0,0 +1,174 @@ +/* + 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 "ewsupdateitemstagsjob.h" + +#include +#include +#include +#include + +#include "ewsid.h" +#include "ewsupdateitemrequest.h" +#include "ewsitemhandler.h" +#include "ewsclient.h" +#include "ewstagstore.h" +#include "ewsresource.h" +#include "ewsglobaltagswritejob.h" + +using namespace Akonadi; + +EwsUpdateItemsTagsJob::EwsUpdateItemsTagsJob(const Akonadi::Item::List &items, EwsTagStore *tagStore, + EwsClient &client, EwsResource *parent) + : EwsJob(parent), mItems(items), mTagStore(tagStore), mClient(client) +{ +} + +EwsUpdateItemsTagsJob::~EwsUpdateItemsTagsJob() +{ +} + +void EwsUpdateItemsTagsJob::start() +{ + Tag::List unknownTags; + + /* In the Exchange world it is not possible to add or remove individual tags from an item + * - it is necessary to write the full list of tags at once. Unfortunately the tags objects + * attached to an item only contain the id. They're missing any persistent identification such + * as the uid. If the EWS resource hasn't seen these tags yet it is necessary to fetch them + * first before any further processing. + */ + Q_FOREACH (const Item &item, mItems) { + Q_FOREACH (const Tag &tag, item.tags()) { + if (!mTagStore->containsId(tag.id())) { + unknownTags.append(tag); + } + } + } + + if (!unknownTags.empty()) { + TagFetchJob *job = new TagFetchJob(unknownTags, this); + connect(job, &TagFetchJob::result, this, &EwsUpdateItemsTagsJob::itemsTagsChangedTagsFetched); + } else { + doUpdateItemsTags(); + } +} + +void EwsUpdateItemsTagsJob::itemsTagsChangedTagsFetched(KJob *job) +{ + Item::List items = job->property("items").value(); + + if (job->error()) { + setErrorMsg(job->errorString()); + emitResult(); + return; + } + + TagFetchJob *tagJob = qobject_cast(job); + if (!tagJob) { + setErrorMsg(QStringLiteral("Invalid TagFetchJob job object")); + emitResult(); + return; + } + + /* All unknown tags have been fetched and can be written to Exchange. */ + mTagStore->addTags(tagJob->tags()); + + EwsResource *res = qobject_cast(parent()); + Q_ASSERT(res); + EwsGlobalTagsWriteJob *writeJob = new EwsGlobalTagsWriteJob(mTagStore, mClient, + res->rootCollection(), this); + connect(writeJob, &EwsGlobalTagsWriteJob::result, this, + &EwsUpdateItemsTagsJob::globalTagsWriteFinished); + writeJob->start(); +} + +void EwsUpdateItemsTagsJob::globalTagsWriteFinished(KJob *job) +{ + if (job->error()) { + setErrorMsg(job->errorString()); + emitResult(); + return; + } + + doUpdateItemsTags(); +} + +void EwsUpdateItemsTagsJob::doUpdateItemsTags() +{ + EwsUpdateItemRequest *req = new EwsUpdateItemRequest(mClient, this); + Q_FOREACH (const Item &item, mItems) { + EwsUpdateItemRequest::ItemChange ic(EwsId(item.remoteId(), item.remoteRevision()), + EwsItemHandler::mimeToItemType(item.mimeType())); + if (!item.tags().isEmpty()) { + QStringList tagList; + QStringList categoryList; + Q_FOREACH (const Tag &tag, item.tags()) { + Q_ASSERT(mTagStore->containsId(tag.id())); + tagList.append(QString::fromLatin1(mTagStore->tagRemoteId(tag.id()))); + QString name = mTagStore->tagName(tag.id()); + if (!name.isEmpty()) { + categoryList.append(name); + } + } + EwsUpdateItemRequest::Update *upd + = new EwsUpdateItemRequest::SetUpdate(EwsResource::tagsProperty, tagList); + ic.addUpdate(upd); + upd = new EwsUpdateItemRequest::SetUpdate(EwsPropertyField(QStringLiteral("item:Categories")), categoryList); + ic.addUpdate(upd); + } else { + EwsUpdateItemRequest::Update *upd + = new EwsUpdateItemRequest::DeleteUpdate(EwsResource::tagsProperty); + ic.addUpdate(upd); + upd = new EwsUpdateItemRequest::DeleteUpdate(EwsPropertyField(QStringLiteral("item:Categories"))); + ic.addUpdate(upd); + } + req->addItemChange(ic); + } + + connect(req, &EwsUpdateItemRequest::result, this, + &EwsUpdateItemsTagsJob::updateItemsTagsRequestFinished); + req->start(); +} + +void EwsUpdateItemsTagsJob::updateItemsTagsRequestFinished(KJob *job) +{ + if (job->error()) { + setErrorMsg(job->errorString()); + emitResult(); + return; + } + + EwsUpdateItemRequest *req = qobject_cast(job); + if (!req) { + setErrorMsg(QStringLiteral("Invalid EwsUpdateItemRequest job object")); + return; + } + + Q_ASSERT(mItems.count() == req->responses().count()); + + auto itemIt = mItems.begin(); + Q_FOREACH (const EwsUpdateItemRequest::Response &resp, req->responses()) { + if (resp.isSuccess()) { + itemIt->setRemoteRevision(resp.itemId().changeKey()); + } + } + + emitResult(); +} diff --git a/resources/ews/task/ewscreatetaskjob.h b/resources/ews/task/ewscreatetaskjob.h new file mode 100644 --- /dev/null +++ b/resources/ews/task/ewscreatetaskjob.h @@ -0,0 +1,37 @@ +/* + 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. +*/ + +#ifndef EWSCREATETASKJOB_H +#define EWSCREATETASKJOB_H + +#include "ewscreateitemjob.h" + +class EwsCreateTaskJob : public EwsCreateItemJob +{ + Q_OBJECT +public: + EwsCreateTaskJob(EwsClient &client, const Akonadi::Item &item, + const Akonadi::Collection &collection, EwsTagStore *tagStore, EwsResource *parent); + virtual ~EwsCreateTaskJob(); + virtual bool setSend(bool send = true) override; +protected: + virtual void doStart() override; +}; + +#endif diff --git a/resources/ews/task/ewscreatetaskjob.cpp b/resources/ews/task/ewscreatetaskjob.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/task/ewscreatetaskjob.cpp @@ -0,0 +1,46 @@ +/* + 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 "ewscreatetaskjob.h" + +#include "ewsresource_debug.h" + +EwsCreateTaskJob::EwsCreateTaskJob(EwsClient &client, const Akonadi::Item &item, + const Akonadi::Collection &collection, EwsTagStore *tagStore, + EwsResource *parent) + : EwsCreateItemJob(client, item, collection, tagStore, parent) +{ +} +EwsCreateTaskJob::~EwsCreateTaskJob() +{ +} + +void EwsCreateTaskJob::doStart() +{ + qCWarning(EWSRES_LOG) << QStringLiteral("Task item creation not implemented!"); + emitResult(); +} + +bool EwsCreateTaskJob::setSend(bool send) +{ + Q_UNUSED(send) + + qCWarning(EWSRES_LOG) << QStringLiteral("Sending task items is not supported!"); + return false; +} diff --git a/resources/ews/task/ewsfetchtaskdetailjob.h b/resources/ews/task/ewsfetchtaskdetailjob.h new file mode 100644 --- /dev/null +++ b/resources/ews/task/ewsfetchtaskdetailjob.h @@ -0,0 +1,35 @@ +/* + 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. +*/ + +#ifndef EWSFETCHTASKDETAILJOB_H +#define EWSFETCHTASKDETAILJOB_H + +#include "ewsfetchitemdetailjob.h" + +class EwsFetchTaskDetailJob : public EwsFetchItemDetailJob +{ + Q_OBJECT +public: + EwsFetchTaskDetailJob(EwsClient &client, QObject *parent, const Akonadi::Collection &collection); + virtual ~EwsFetchTaskDetailJob(); +protected: + virtual void processItems(const QList &responses) override; +}; + +#endif diff --git a/resources/ews/task/ewsfetchtaskdetailjob.cpp b/resources/ews/task/ewsfetchtaskdetailjob.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/task/ewsfetchtaskdetailjob.cpp @@ -0,0 +1,62 @@ +/* + 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 "ewsfetchtaskdetailjob.h" + +#include "ewsitemshape.h" +#include "ewsgetitemrequest.h" +#include "ewsmailbox.h" +#include "ewsresource_debug.h" + +using namespace Akonadi; + +EwsFetchTaskDetailJob::EwsFetchTaskDetailJob(EwsClient &client, QObject *parent, const Akonadi::Collection &collection) + : EwsFetchItemDetailJob(client, parent, collection) +{ + EwsItemShape shape(EwsShapeIdOnly); + mRequest->setItemShape(shape); +} + + +EwsFetchTaskDetailJob::~EwsFetchTaskDetailJob() +{ +} + +void EwsFetchTaskDetailJob::processItems(const QList &responses) +{ + Item::List::iterator it = mChangedItems.begin(); + + Q_FOREACH (const EwsGetItemRequest::Response &resp, responses) { + Item &item = *it; + + if (!resp.isSuccess()) { + qCWarningNC(EWSRES_LOG) << QStringLiteral("Failed to fetch item %1").arg(item.remoteId()); + continue; + } + + //const EwsItem &ewsItem = resp.item(); + + // TODO: Implement + + ++it; + } + + emitResult(); +} + diff --git a/resources/ews/task/ewsmodifytaskjob.h b/resources/ews/task/ewsmodifytaskjob.h new file mode 100644 --- /dev/null +++ b/resources/ews/task/ewsmodifytaskjob.h @@ -0,0 +1,35 @@ +/* + 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. +*/ + +#ifndef EWSMODIFYTASKJOB_H +#define EWSMODIFYTASKJOB_H + +#include "ewsmodifyitemjob.h" + +class EwsModifyTaskJob : public EwsModifyItemJob +{ + Q_OBJECT +public: + EwsModifyTaskJob(EwsClient &client, const Akonadi::Item::List &items, const QSet &parts, + QObject *parent); + virtual ~EwsModifyTaskJob(); + virtual void start() override; +}; + +#endif diff --git a/resources/ews/task/ewsmodifytaskjob.cpp b/resources/ews/task/ewsmodifytaskjob.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/task/ewsmodifytaskjob.cpp @@ -0,0 +1,37 @@ +/* + 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 "ewsmodifytaskjob.h" + +#include "ewsresource_debug.h" + +EwsModifyTaskJob::EwsModifyTaskJob(EwsClient &client, const Akonadi::Item::List &items, + const QSet &parts, QObject *parent) + : EwsModifyItemJob(client, items, parts, parent) +{ +} +EwsModifyTaskJob::~EwsModifyTaskJob() +{ +} + +void EwsModifyTaskJob::start() +{ + qCWarning(EWSRES_LOG) << QStringLiteral("Task item modification not implemented!"); + emitResult(); +} diff --git a/resources/ews/task/ewstaskhandler.h b/resources/ews/task/ewstaskhandler.h new file mode 100644 --- /dev/null +++ b/resources/ews/task/ewstaskhandler.h @@ -0,0 +1,45 @@ +/* + 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. +*/ + +#ifndef EWSTASKHANDLER_H +#define EWSTASKHANDLER_H + +#include "ewsitemhandler.h" + +class EwsTaskHandler : public EwsItemHandler +{ +public: + EwsTaskHandler(); + virtual ~EwsTaskHandler(); + + virtual EwsFetchItemDetailJob *fetchItemDetailJob(EwsClient &client, QObject *parent, + const Akonadi::Collection &collection) override; + virtual void setSeenFlag(Akonadi::Item &item, bool value) override; + virtual QString mimeType() override; + virtual bool setItemPayload(Akonadi::Item &item, const EwsItem &ewsItem) override; + virtual EwsModifyItemJob *modifyItemJob(EwsClient &client, const QVector &items, + const QSet &parts, QObject *parent) override; + virtual EwsCreateItemJob *createItemJob(EwsClient &client, const Akonadi::Item &item, + const Akonadi::Collection &collection, + EwsTagStore *tagStore, EwsResource *parent) override; + static EwsItemHandler *factory(); +private: +}; + +#endif diff --git a/resources/ews/task/ewstaskhandler.cpp b/resources/ews/task/ewstaskhandler.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/task/ewstaskhandler.cpp @@ -0,0 +1,80 @@ +/* + 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 "ewstaskhandler.h" + +#include + +#include "ewsfetchtaskdetailjob.h" +#include "ewsmodifytaskjob.h" +#include "ewscreatetaskjob.h" + +using namespace Akonadi; + +EwsTaskHandler::EwsTaskHandler() +{ +} + +EwsTaskHandler::~EwsTaskHandler() +{ +} + +EwsItemHandler *EwsTaskHandler::factory() +{ + return new EwsTaskHandler(); +} + +EwsFetchItemDetailJob *EwsTaskHandler::fetchItemDetailJob(EwsClient &client, QObject *parent, + const Akonadi::Collection &collection) +{ + return new EwsFetchTaskDetailJob(client, parent, collection); +} + +void EwsTaskHandler::setSeenFlag(Item &item, bool value) +{ + Q_UNUSED(item) + Q_UNUSED(value) +} + +QString EwsTaskHandler::mimeType() +{ + return KCalCore::Todo::todoMimeType(); +} + +bool EwsTaskHandler::setItemPayload(Akonadi::Item &item, const EwsItem &ewsItem) +{ + Q_UNUSED(item) + Q_UNUSED(ewsItem) + + return true; +} + +EwsModifyItemJob *EwsTaskHandler::modifyItemJob(EwsClient &client, const QVector &items, + const QSet &parts, QObject *parent) +{ + return new EwsModifyTaskJob(client, items, parts, parent); +} + +EwsCreateItemJob *EwsTaskHandler::createItemJob(EwsClient &client, const Akonadi::Item &item, + const Akonadi::Collection &collection, EwsTagStore *tagStore, EwsResource *parent) +{ + return new EwsCreateTaskJob(client, item, collection, tagStore, parent); +} + +EWS_DECLARE_ITEM_HANDLER(EwsTaskHandler, EwsItemTypeTask) diff --git a/resources/ews/test/CMakeLists.txt b/resources/ews/test/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/resources/ews/test/CMakeLists.txt @@ -0,0 +1,94 @@ +# +# 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_directories(../) +include_directories(${CMAKE_CURRENT_BINARY_DIR}/../) +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/fakeserver) + +set(isolatestestcommon_SRCS isolatedtestbase.cpp statemonitor.cpp) + +kcfg_generate_dbus_interface(${CMAKE_CURRENT_SOURCE_DIR}/../ewsresource.kcfg org.kde.Akonadi.Ews.Settings) +set(ewssettingsinterface_xml ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.Ews.Settings.xml) +qt5_generate_dbus_interface( ${CMAKE_CURRENT_SOURCE_DIR}/../settings.h org.kde.Akonadi.Ews.Wallet.xml OPTIONS -a ) +set(ewswalletinterface_xml ${CMAKE_CURRENT_BINARY_DIR}/org.kde.Akonadi.Ews.Wallet.xml) + +qt5_add_dbus_interface(isolatestestcommon_SRCS ${ewssettingsinterface_xml} ewssettings) +qt5_add_dbus_interface(isolatestestcommon_SRCS ${ewswalletinterface_xml} ewswallet) + +qt5_add_resources(isolatestestcommon_RSRCS isolatedtestcommon.qrc) + +add_library(isolatedtestcommon STATIC ${isolatestestcommon_SRCS}) +target_link_libraries(isolatedtestcommon + KF5::AkonadiCore + Qt5::Core + Qt5::Network + Qt5::Test +) + +macro(add_akonadi_isolated_test _source) + get_filename_component(_targetName ${_source} NAME_WE) + set(_srcList ${_source}) + if(EXISTS ${_targetName}.qrc) + qt5_add_resources(_srcList ${_targetName}.qrc) + endif(EXISTS ${_targetName}.qrc) + + add_executable(${_targetName} ${_srcList} ${isolatestestcommon_RSRCS}) + target_link_libraries(${_targetName} + Qt5::Test + KF5::AkonadiCore + KF5::Mime + Qt5::Network + Qt5::Widgets + Qt5::DBus + fakeewsserver + isolatedtestcommon + ) + + # based on kde4_add_unit_test + if (WIN32) + get_target_property( _loc ${_targetName} LOCATION ) + set(_executable ${_loc}.bat) + else() + set(_executable ${CMAKE_CURRENT_BINARY_DIR}/${_targetName}) + endif() + if (UNIX) + if (APPLE) + set(_executable ${_executable}.app/Contents/MacOS/${_targetName}) + else() + set(_executable ${_executable}) + endif() + endif() + + find_program(_testrunner akonaditest) + + add_test( ${_targetName} ${_testrunner} -c ${CMAKE_CURRENT_BINARY_DIR}/config.xml ${_executable} ) +endmacro(add_akonadi_isolated_test) + +add_subdirectory(unittests) +add_subdirectory(fakeserver) + +file(TO_NATIVE_PATH ${CMAKE_CURRENT_BINARY_DIR}/xdgdata EWS_TMP_XDG_DATA_DIR) +file(TO_NATIVE_PATH ${EWS_TMP_XDG_DATA_DIR}/akonadi/agents EWS_TMP_AGENTS_DIR) +file(MAKE_DIRECTORY ${EWS_TMP_XDG_DATA_DIR}) +file(COPY ${CMAKE_CURRENT_BINARY_DIR}/../ewsresource.desktop DESTINATION ${EWS_TMP_AGENTS_DIR}) + +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/testenv/config.xml.cmake ${CMAKE_CURRENT_BINARY_DIR}/config.xml) + +# Disable for now - needs to be properly ported to kdepim-runtime +#add_akonadi_isolated_test(basictest.cpp) + diff --git a/resources/ews/test/basictest.cpp b/resources/ews/test/basictest.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/test/basictest.cpp @@ -0,0 +1,162 @@ +/* + 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 +#include +#include +#include +#include +#include +#include + +#include "fakeewsserverthread.h" +#include "ewssettings.h" +#include "ewswallet.h" +#include "isolatedtestbase.h" +#include "statemonitor.h" + +class BasicTest : public IsolatedTestBase +{ + Q_OBJECT +public: + explicit BasicTest(QObject *parent = nullptr); + virtual ~BasicTest(); +private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + void testBasic(); +}; + +QTEST_AKONADIMAIN(BasicTest) + +using namespace Akonadi; + +BasicTest::BasicTest(QObject *parent) + : IsolatedTestBase(parent) +{ +} + +BasicTest::~BasicTest() +{ +} + +void BasicTest::initTestCase() +{ + init(); +} + +void BasicTest::cleanupTestCase() +{ + cleanup(); +} + +void BasicTest::testBasic() +{ + static const auto rootId = QStringLiteral("cm9vdA=="); + static const auto inboxId = QStringLiteral("aW5ib3g="); + FolderList folderList = { + {rootId, mEwsInstance->identifier(), Folder::Root, QString()}, + {inboxId, QStringLiteral("Inbox"), Folder::Inbox, rootId}, + {QStringLiteral("Y2FsZW5kYXI="), QStringLiteral("Calendar"), Folder::Calendar, rootId}, + {QStringLiteral("dGFza3M="), QStringLiteral("Tasks"), Folder::Tasks, rootId}, + {QStringLiteral("Y29udGFjdHM="), QStringLiteral("Contacts"), Folder::Contacts, rootId}, + {QStringLiteral("b3V0Ym94"), QStringLiteral("Outbox"), Folder::Outbox, rootId}, + {QStringLiteral("c2VudCBpdGVtcw=="), QStringLiteral("Sent Items"), Folder::Sent, rootId}, + {QStringLiteral("ZGVsZXRlZCBpdGVtcw=="), QStringLiteral("Deleted Items"), Folder::Trash, rootId}, + {QStringLiteral("ZHJhZnRz"), QStringLiteral("Drafts"), Folder::Drafts, rootId} + }; + + struct DesiredState { + QString parentId; + QString iconName; + }; + QHash desiredStates = { + {rootId, {QString(), QString()}}, + {inboxId, {rootId, QStringLiteral("mail-folder-inbox")}}, + {QStringLiteral("Y2FsZW5kYXI="), {rootId, QString()}}, + {QStringLiteral("dGFza3M="), {rootId, QString()}}, + {QStringLiteral("Y29udGFjdHM="), {rootId, QString()}}, + {QStringLiteral("b3V0Ym94"), {rootId, QStringLiteral("mail-folder-outbox")}}, + {QStringLiteral("c2VudCBpdGVtcw=="), {rootId, QStringLiteral("mail-folder-sent")}}, + {QStringLiteral("ZGVsZXRlZCBpdGVtcw=="), {rootId, QStringLiteral("user-trash")}}, + {QStringLiteral("ZHJhZnRz"), {rootId, QStringLiteral("document-properties")}} + }; + + FakeEwsServer::DialogEntry::List dialog = { + MsgRootInboxDialogEntry(rootId, inboxId, + QStringLiteral("GetFolder request for inbox and msgroot")), + SubscribedFoldersDialogEntry(folderList, + QStringLiteral("GetFolder request for subscribed folders")), + SpecialFoldersDialogEntry(folderList, + QStringLiteral("GetFolder request for special folders")), + GetTagsEmptyDialogEntry(rootId, + QStringLiteral("GetFolder request for tags")), + SubscribeStreamingDialogEntry(QStringLiteral("Subscribe request for streaming events")), + SyncFolderHierInitialDialogEntry(folderList, QStringLiteral("bNUUPDWHTvuG9p57NGZdhjREdZXDt48a0E1F22yThko="), + QStringLiteral("SyncFolderHierarchy request with empty state")), + UnsubscribeDialogEntry(QStringLiteral("Unsubscribe request")) + }; + + bool unknownRequestEncountered = false; + mFakeServerThread->setDialog(dialog); + mFakeServerThread->setDefaultReplyCallback([&](const QString & req, QXmlResultItems &, const QXmlNamePool &) { + qDebug() << "Unknown EWS request encountered." << req; + unknownRequestEncountered = true; + return FakeEwsServer::EmptyResponse; + }); + + QEventLoop loop; + + CollectionStateMonitor stateMonitor(this, desiredStates, inboxId, + [](const Collection &col, const DesiredState &state) { + auto attr = col.attribute(); + QString iconName; + if (attr) { + iconName = attr->iconName(); + } + return col.parentCollection().remoteId() == state.parentId && iconName == state.iconName; + }); + + stateMonitor.monitor().fetchCollection(true); + stateMonitor.monitor().collectionFetchScope().fetchAttribute(); + stateMonitor.monitor().collectionFetchScope().setAncestorRetrieval(CollectionFetchScope::Parent); + stateMonitor.monitor().setResourceMonitored(mEwsInstance->identifier().toAscii(), true); + connect(&stateMonitor, &CollectionStateMonitor::stateReached, this, [&]() { + loop.exit(0); + }); + connect(&stateMonitor, &CollectionStateMonitor::errorOccurred, this, [&]() { + loop.exit(1); + }); + + QVERIFY(setEwsResOnline(true, true)); + + QTimer timer; + timer.setSingleShot(true); + connect(&timer, &QTimer::timeout, this, [&]() { + qDebug() << "Timeout waiting for desired resource online state."; + loop.exit(1); + }); + timer.start(5000); + QCOMPARE(loop.exec(), 0); + + QVERIFY(!unknownRequestEncountered); + QVERIFY(setEwsResOnline(false, true)); +} + +#include "basictest.moc" diff --git a/resources/ews/test/fakeserver/CMakeLists.txt b/resources/ews/test/fakeserver/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/resources/ews/test/fakeserver/CMakeLists.txt @@ -0,0 +1,29 @@ +# +# 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. + +set(fakeewsserver_SRCS + fakeewsconnection.cpp + fakeewsserver_debug.cpp + fakeewsserver.cpp + fakeewsserverthread.cpp) + +add_library(fakeewsserver ${fakeewsserver_SRCS}) + +target_link_libraries(fakeewsserver Qt5::Core Qt5::Network Qt5::XmlPatterns) + +add_subdirectory(test) diff --git a/resources/ews/test/fakeserver/fakeewsconnection.h b/resources/ews/test/fakeserver/fakeewsconnection.h new file mode 100644 --- /dev/null +++ b/resources/ews/test/fakeserver/fakeewsconnection.h @@ -0,0 +1,72 @@ +/* + 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 FAKEEWSCONNECTION_H +#define FAKEEWSCONNECTION_H + +#include +#include +#include + +#include "fakeewsserver.h" + +class QTcpSocket; + +class FakeEwsConnection : public QObject +{ + Q_OBJECT +public: + + FakeEwsConnection(QTcpSocket *sock, FakeEwsServer *parent); + virtual ~FakeEwsConnection(); + void sendEvents(const QStringList &events); +private Q_SLOTS: + void disconnected(); + void dataAvailable(); + void dataTimeout(); + void streamingRequestTimeout(); + void streamingRequestHeartbeat(); +Q_SIGNALS: + void streamingRequestStarted(FakeEwsConnection *conn); +private: + void sendError(const QString &msg, ushort code = 500); + FakeEwsServer::DialogEntry::HttpResponse parseRequest(const QString &content); + FakeEwsServer::DialogEntry::HttpResponse handleGetEventsRequest(const QString &content); + FakeEwsServer::DialogEntry::HttpResponse handleGetStreamingEventsRequest(const QString &content); + QString prepareEventsResponse(const QStringList &events); + + enum HttpConnectionState { + Initial, + RequestReceived, + HeadersReceived + }; + + QPointer mSock; + uint mContentLength; + QByteArray mContent; + QTimer mDataTimer; + bool mKeepAlive; + QTimer mStreamingRequestHeartbeat; + QTimer mStreamingRequestTimeout; + QString mStreamingSubId; + HttpConnectionState mState; + bool mAuthenticated; +}; + +#endif diff --git a/resources/ews/test/fakeserver/fakeewsconnection.cpp b/resources/ews/test/fakeserver/fakeewsconnection.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/test/fakeserver/fakeewsconnection.cpp @@ -0,0 +1,438 @@ +/* + 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 "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(QLatin1String("Invalid request header")); + return; + } + if (tokens.at(0) != "POST") { + sendError(QLatin1String("Expected POST request")); + return; + } + if (tokens.at(1) != "/EWS/Exchange.asmx") { + sendError(QLatin1String("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(QLatin1String("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(QLatin1String("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 }; + } + + 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(QLatin1String("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 += QStringLiteral("") + match.captured(QStringLiteral("subid")) + QStringLiteral(""); + resp += QStringLiteral("") + match.captured(QStringLiteral("watermark")) + QStringLiteral(""); + resp += QStringLiteral("false"); + + FakeEwsServer *server = qobject_cast(parent()); + const QStringList events = server->retrieveEventsXml(); + qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Returning %1 events.").arg(events.size()); + Q_FOREACH (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 += QStringLiteral("") + mStreamingSubId + QStringLiteral(""); + + 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/fakeewsserver.h b/resources/ews/test/fakeserver/fakeewsserver.h new file mode 100644 --- /dev/null +++ b/resources/ews/test/fakeserver/fakeewsserver.h @@ -0,0 +1,80 @@ +/* + 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 FAKEEWSSERVER_H +#define FAKEEWSSERVER_H + +#include + +#include +#include +#include +#include +#include + +class FakeEwsConnection; +class QXmlResultItems; +class QXmlNamePool; + +class Q_DECL_EXPORT FakeEwsServer : public QTcpServer +{ + Q_OBJECT +public: + class DialogEntry + { + public: + typedef QPair HttpResponse; + typedef std::function ReplyCallback; + QString xQuery; + ReplyCallback replyCallback; + QString description; + + typedef QVector List; + }; + + static const DialogEntry::HttpResponse EmptyResponse; + + explicit FakeEwsServer(QObject *parent); + virtual ~FakeEwsServer(); + bool start(); + void setDefaultReplyCallback(DialogEntry::ReplyCallback defaultReplyCallback); + void queueEventsXml(const QStringList &events); + void setDialog(const DialogEntry::List &dialog); + ushort portNumber() const; +private Q_SLOTS: + void newConnectionReceived(); + void streamingConnectionStarted(FakeEwsConnection *conn); +private: + void dataAvailable(QTcpSocket *sock); + void sendError(QTcpSocket *sock, const QString &msg, ushort code = 500); + const DialogEntry::List dialog() const; + const DialogEntry::ReplyCallback defaultReplyCallback() const; + QStringList retrieveEventsXml(); + + DialogEntry::List mDialog; + DialogEntry::ReplyCallback mDefaultReplyCallback; + QStringList mEventQueue; + QPointer mStreamingEventsConnection; + ushort mPortNumber; + mutable QMutex mMutex; + + friend class FakeEwsConnection; +}; + +#endif diff --git a/resources/ews/test/fakeserver/fakeewsserver.cpp b/resources/ews/test/fakeserver/fakeewsserver.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/test/fakeserver/fakeewsserver.cpp @@ -0,0 +1,135 @@ +/* + 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 "fakeewsserver.h" + +#include +#include + +#include "fakeewsconnection.h" +#include "fakeewsserver_debug.h" + +const FakeEwsServer::DialogEntry::HttpResponse FakeEwsServer::EmptyResponse = {QString(), 0}; + +FakeEwsServer::FakeEwsServer(QObject *parent) + : QTcpServer(parent), mPortNumber(0) +{ +} +FakeEwsServer::~FakeEwsServer() +{ + qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Stopping fake EWS server."); +} + +bool FakeEwsServer::start() +{ + QMutexLocker lock(&mMutex); + + int retries = 3; + bool ok; + do { + mPortNumber = (qrand() % 10000) + 10000; + qCInfoNC(EWSFAKE_LOG) << QStringLiteral("Starting fake EWS server at 127.0.0.1:%1").arg(mPortNumber); + ok = listen(QHostAddress::LocalHost, mPortNumber); + if (!ok) { + qCWarningNC(EWSFAKE_LOG) << QStringLiteral("Failed to start server"); + } + } while (!ok && --retries); + + if (ok) { + connect(this, &QTcpServer::newConnection, this, &FakeEwsServer::newConnectionReceived); + } + + return ok; +} + +void FakeEwsServer::setDialog(const DialogEntry::List &dialog) +{ + QMutexLocker lock(&mMutex); + + mDialog = dialog; +} + +void FakeEwsServer::setDefaultReplyCallback(DialogEntry::ReplyCallback defaultReplyCallback) +{ + QMutexLocker lock(&mMutex); + + mDefaultReplyCallback = defaultReplyCallback; +} + +void FakeEwsServer::queueEventsXml(const QStringList &events) +{ + if (QThread::currentThread() != thread()) { + qCWarningNC(EWSFAKE_LOG) << QStringLiteral("queueEventsXml called from wrong thread " + "(called from ") << QThread::currentThread() << QStringLiteral(", should be ") + << thread() << QStringLiteral(")"); + return; + } + mEventQueue += events; + + if (mStreamingEventsConnection) { + mStreamingEventsConnection->sendEvents(mEventQueue); + mEventQueue.clear(); + } +} + +QStringList FakeEwsServer::retrieveEventsXml() +{ + QStringList events = mEventQueue; + mEventQueue.clear(); + return events; +} + +void FakeEwsServer::newConnectionReceived() +{ + QTcpSocket *sock = nextPendingConnection(); + + FakeEwsConnection *conn = new FakeEwsConnection(sock, this); + connect(conn, &FakeEwsConnection::streamingRequestStarted, this, &FakeEwsServer::streamingConnectionStarted); +} + +const FakeEwsServer::DialogEntry::List FakeEwsServer::dialog() const +{ + QMutexLocker lock(&mMutex); + + return mDialog; +} + +const FakeEwsServer::DialogEntry::ReplyCallback FakeEwsServer::defaultReplyCallback() const +{ + QMutexLocker lock(&mMutex); + + return mDefaultReplyCallback; +} + +void FakeEwsServer::streamingConnectionStarted(FakeEwsConnection *conn) +{ + if (mStreamingEventsConnection) { + qCWarningNC(EWSFAKE_LOG) << QStringLiteral("Got new streaming connection while existing one is active - terminating existing one"); + mStreamingEventsConnection->deleteLater(); + } + + mStreamingEventsConnection = conn; +} + +ushort FakeEwsServer::portNumber() const +{ + QMutexLocker lock(&mMutex); + + return mPortNumber; +} diff --git a/resources/ews/test/fakeserver/fakeewsserver_debug.h b/resources/ews/test/fakeserver/fakeewsserver_debug.h new file mode 100644 --- /dev/null +++ b/resources/ews/test/fakeserver/fakeewsserver_debug.h @@ -0,0 +1,33 @@ +/* + 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 FAKEEWSSERVER_DEBUG_H +#define FAKEEWSSERVER_DEBUG_H + +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(EWSFAKE_LOG) + +#define qCDebugNC(cat) qCDebug(cat).noquote() +#define qCInfoNC(cat) qCInfo(cat).noquote() +#define qCWarningNC(cat) qCWarning(cat).noquote() +#define qCCriticalNC(cat) qCCritical(cat).noquote() + +#endif diff --git a/resources/ews/test/fakeserver/fakeewsserver_debug.cpp b/resources/ews/test/fakeserver/fakeewsserver_debug.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/test/fakeserver/fakeewsserver_debug.cpp @@ -0,0 +1,23 @@ +/* + 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 "fakeewsserver_debug.h" + +Q_LOGGING_CATEGORY(EWSFAKE_LOG, "log_ews_fakeserver") + diff --git a/resources/ews/test/fakeserver/fakeewsserverthread.h b/resources/ews/test/fakeserver/fakeewsserverthread.h new file mode 100644 --- /dev/null +++ b/resources/ews/test/fakeserver/fakeewsserverthread.h @@ -0,0 +1,61 @@ +/* + 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 = 0); + virtual ~FakeEwsServerThread(); + +// 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 queueEventsXml(const QStringList &events); + bool waitServerStarted() const; +Q_SIGNALS: + void serverStarted(bool ok); +protected: + virtual 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/fakeserver/fakeewsserverthread.cpp b/resources/ews/test/fakeserver/fakeewsserverthread.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/test/fakeserver/fakeewsserverthread.cpp @@ -0,0 +1,109 @@ +/* + 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(0)); + 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) +{ + 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/test/CMakeLists.txt b/resources/ews/test/fakeserver/test/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/resources/ews/test/fakeserver/test/CMakeLists.txt @@ -0,0 +1,27 @@ +# +# Copyright (C) 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_directories(${CMAKE_CURRENT_SOURCE_DIR}/..) + +macro(akonadi_fakeserver_add_test tname) + add_executable(${tname} ${tname}.cpp) + target_link_libraries(${tname} Qt5::Test fakeewsserver) + add_test(NAME ${tname} COMMAND ${tname}) +endmacro(akonadi_fakeserver_add_test tname) + +akonadi_fakeserver_add_test(ewsfakesrv_test) diff --git a/resources/ews/test/fakeserver/test/ewsfakesrv_test.cpp b/resources/ews/test/fakeserver/test/ewsfakesrv_test.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/test/fakeserver/test/ewsfakesrv_test.cpp @@ -0,0 +1,880 @@ +/* + 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 +#include +#include +#include +#include +#include +#include +#include +#include + +#include "fakeewsserver.h" +#include "fakeewsserverthread.h" + +class UtEwsFakeSrvTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void emptyDialog(); + void invalidURL(); + void invalidMethod(); + void emptyRequest(); + void defaultCallback(); + void simpleResponse(); + void callbackResponse(); + void multipleResponses(); + void emptyResponse(); + void getEventsRequest(); + void getEventsRequest_data(); + void getStreamingEventsRequest(); + void serverThread(); + void delayedContentSize(); + void notAuthenticated(); + void badAuthentication(); + void xqueryResultsInCallback(); +private: + QPair synchronousHttpReq(const QString &content, ushort port, + std::function chunkFn = nullptr); +}; + +void UtEwsFakeSrvTest::emptyDialog() +{ + QScopedPointer srv(new FakeEwsServer(this)); + QVERIFY(srv->start()); + + QNetworkAccessManager nam(this); + QUrl url(QStringLiteral("http://127.0.0.1:%1/EWS/Exchange.asmx").arg(srv->portNumber())); + url.setUserName(QStringLiteral("test")); + url.setPassword(QStringLiteral("test")); + QNetworkRequest req(url); + req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("text/xml")); + QNetworkReply *reply = nam.post(req, "test"); + + QEventLoop loop; + QString resp; + ushort respCode = 0; + + connect(reply, &QNetworkReply::readyRead, this, [reply, &resp]() { + resp += QString::fromUtf8(reply->readAll()); + }); + connect(reply, &QNetworkReply::finished, this, [&loop, reply]() { + loop.exit(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toUInt()); + }); + respCode = loop.exec(); + + QCOMPARE(respCode, static_cast(500)); +} + +void UtEwsFakeSrvTest::invalidURL() +{ + QScopedPointer srv(new FakeEwsServer(this)); + QVERIFY(srv->start()); + + QNetworkAccessManager nam(this); + QUrl url(QStringLiteral("http://127.0.0.1:%1/some/url").arg(srv->portNumber())); + url.setUserName(QStringLiteral("test")); + url.setPassword(QStringLiteral("test")); + QNetworkRequest req(url); + req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("text/xml")); + QNetworkReply *reply = nam.post(req, "test"); + + QEventLoop loop; + QString resp; + ushort respCode = 0; + + connect(reply, &QNetworkReply::readyRead, this, [reply, &resp]() { + resp += QString::fromUtf8(reply->readAll()); + }); + connect(reply, &QNetworkReply::finished, this, [&loop, reply]() { + loop.exit(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toUInt()); + }); + respCode = loop.exec(); + + QCOMPARE(respCode, static_cast(500)); +} + +void UtEwsFakeSrvTest::invalidMethod() +{ + QScopedPointer srv(new FakeEwsServer(this)); + QVERIFY(srv->start()); + + QNetworkAccessManager nam(this); + QUrl url(QStringLiteral("http://127.0.0.1:%1/EWS/Exchange.asmx").arg(srv->portNumber())); + url.setUserName(QStringLiteral("test")); + url.setPassword(QStringLiteral("test")); + QNetworkRequest req(url); + QNetworkReply *reply = nam.get(req); + + QEventLoop loop; + QString resp; + ushort respCode = 0; + + connect(reply, &QNetworkReply::readyRead, this, [reply, &resp]() { + resp += QString::fromUtf8(reply->readAll()); + }); + connect(reply, &QNetworkReply::finished, this, [&loop, reply]() { + loop.exit(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toUInt()); + }); + respCode = loop.exec(); + + QCOMPARE(respCode, static_cast(500)); +} + +void UtEwsFakeSrvTest::emptyRequest() +{ + QScopedPointer srv(new FakeEwsServer(this)); + QVERIFY(srv->start()); + + auto resp = synchronousHttpReq(QStringLiteral(""), srv->portNumber()); + QCOMPARE(resp.second, static_cast(500)); +} + +void UtEwsFakeSrvTest::defaultCallback() +{ + QString receivedReq; + QScopedPointer srv(new FakeEwsServer(this)); + srv->setDefaultReplyCallback([&receivedReq](const QString & req, QXmlResultItems &, const QXmlNamePool &) { + receivedReq = req; + return FakeEwsServer::DialogEntry::HttpResponse(QStringLiteral("testresp"), 200); + }); + QVERIFY(srv->start()); + + auto resp = synchronousHttpReq(QStringLiteral("testreq"), srv->portNumber()); + QCOMPARE(receivedReq, QStringLiteral("testreq")); + QCOMPARE(resp.first, QStringLiteral("testresp")); + QCOMPARE(resp.second, static_cast(200)); +} + +void UtEwsFakeSrvTest::simpleResponse() +{ + const FakeEwsServer::DialogEntry::List dialog = { + { + QStringLiteral("if (//test1/a = ) then () else ()"), + FakeEwsServer::DialogEntry::ReplyCallback(), + QStringLiteral("Sample request 1") + } + }; + + QString receivedReq; + QScopedPointer srv(new FakeEwsServer(this)); + srv->setDialog(dialog); + QVERIFY(srv->start()); + + auto resp = synchronousHttpReq(QStringLiteral(""), srv->portNumber()); + QCOMPARE(resp.first, QStringLiteral("")); + QCOMPARE(resp.second, static_cast(200)); +} + +void UtEwsFakeSrvTest::callbackResponse() +{ + const FakeEwsServer::DialogEntry::List dialog = { + { + QStringLiteral("if (//test1/a = ) then () else ()"), + [](const QString &, QXmlResultItems &, const QXmlNamePool &) + { + return FakeEwsServer::DialogEntry::HttpResponse(QStringLiteral(""), 200); + }, + QStringLiteral("Sample request 1") + } + }; + + QString receivedReq; + QScopedPointer srv(new FakeEwsServer(this)); + srv->setDialog(dialog); + QVERIFY(srv->start()); + + auto resp = synchronousHttpReq(QStringLiteral(""), srv->portNumber()); + QCOMPARE(resp.first, QStringLiteral("")); + QCOMPARE(resp.second, static_cast(200)); +} + +void UtEwsFakeSrvTest::multipleResponses() +{ + const FakeEwsServer::DialogEntry::List dialog = { + { + QStringLiteral("if (//test1/a = or //test1/b = ) then () else ()"), + FakeEwsServer::DialogEntry::ReplyCallback(), + QStringLiteral("Sample request 1") + }, + { + QStringLiteral("if (//test1/b = or //test1/c = ) then () else ()"), + FakeEwsServer::DialogEntry::ReplyCallback(), + QStringLiteral("Sample request 2") + } + }; + + QString receivedReq; + QScopedPointer srv(new FakeEwsServer(this)); + srv->setDialog(dialog); + QVERIFY(srv->start()); + + auto resp = synchronousHttpReq(QStringLiteral(""), srv->portNumber()); + QCOMPARE(resp.first, QStringLiteral("")); + QCOMPARE(resp.second, static_cast(200)); + + resp = synchronousHttpReq(QStringLiteral(""), srv->portNumber()); + QCOMPARE(resp.first, QStringLiteral("")); + QCOMPARE(resp.second, static_cast(200)); + + resp = synchronousHttpReq(QStringLiteral(""), srv->portNumber()); + QCOMPARE(resp.first, QStringLiteral("")); + QCOMPARE(resp.second, static_cast(200)); + + resp = synchronousHttpReq(QStringLiteral(""), srv->portNumber()); + QCOMPARE(resp.second, static_cast(500)); +} + +void UtEwsFakeSrvTest::emptyResponse() +{ + bool callbackCalled = false; + const FakeEwsServer::DialogEntry::List dialog = { + { + QStringLiteral("if (//test1/a = ) then () else ()"), + [&callbackCalled](const QString &, QXmlResultItems &, const QXmlNamePool &) + { + callbackCalled = true; + return FakeEwsServer::EmptyResponse; + }, + QStringLiteral("Sample request 1") + } + }; + + QString receivedReq; + QScopedPointer srv(new FakeEwsServer(this)); + srv->setDialog(dialog); + QVERIFY(srv->start()); + + auto resp = synchronousHttpReq(QStringLiteral(""), srv->portNumber()); + QCOMPARE(resp.second, static_cast(500)); + QCOMPARE(callbackCalled, true); +} + +void UtEwsFakeSrvTest::getEventsRequest() +{ + const FakeEwsServer::DialogEntry::List emptyDialog; + + const QFETCH(QString, request); + const QFETCH(QStringList, events); + const QFETCH(ushort, responseCode); + const QFETCH(QString, response); + + QScopedPointer srv(new FakeEwsServer(this)); + QVERIFY(srv->start()); + + srv->queueEventsXml(events); + + auto resp = synchronousHttpReq(request, srv->portNumber()); + QCOMPARE(resp.second, responseCode); + if (responseCode == 200) { + QCOMPARE(resp.first, response); + } +} + +void UtEwsFakeSrvTest::getEventsRequest_data() +{ + QTest::addColumn("request"); + QTest::addColumn("events"); + QTest::addColumn("responseCode"); + QTest::addColumn("response"); + + QTest::newRow("valid request (MSDN)") + << QStringLiteral("" + "" + "" + "" + "f6bc657d-dde1-4f94-952d-143b95d6483d" + "AAAAAMAGAAAAAAAAAQ==" + "" + "" + "") + << (QStringList() + << QStringLiteral("AAAAAM4GAAAAAAAAAQ==" + "2006-08-22T00:36:29Z" + "" + "" + "") + << QStringLiteral("AAAAAOQGAAAAAAAAAQ==" + "2006-08-22T01:00:50Z" + "" + "" + "")) + << static_cast(200) + << QStringLiteral("" + "" + "" + "" + "" + "" + "" + "" + "" + "NoError" + "f6bc657d-dde1-4f94-952d-143b95d6483d" + "AAAAAMAGAAAAAAAAAQ==" + "false" + "" + "AAAAAM4GAAAAAAAAAQ==" + "2006-08-22T00:36:29Z" + "" + "" + "" + "" + "AAAAAOQGAAAAAAAAAQ==" + "2006-08-22T01:00:50Z" + "" + "" + "" + "" + "" + "" + "" + "" + ""); + + QTest::newRow("valid request (namespaced)") + << QStringLiteral("" + "" + "" + "" + "f6bc657d-dde1-4f94-952d-143b95d6483d" + "AAAAAMAGAAAAAAAAAQ==" + "" + "" + "") + << (QStringList() + << QStringLiteral("AAAAAM4GAAAAAAAAAQ==" + "2006-08-22T00:36:29Z" + "" + "" + "") + << QStringLiteral("AAAAAOQGAAAAAAAAAQ==" + "2006-08-22T01:00:50Z" + "" + "" + "")) + << static_cast(200) + << QStringLiteral("" + "" + "" + "" + "" + "" + "" + "" + "" + "NoError" + "f6bc657d-dde1-4f94-952d-143b95d6483d" + "AAAAAMAGAAAAAAAAAQ==" + "false" + "" + "AAAAAM4GAAAAAAAAAQ==" + "2006-08-22T00:36:29Z" + "" + "" + "" + "" + "AAAAAOQGAAAAAAAAAQ==" + "2006-08-22T01:00:50Z" + "" + "" + "" + "" + "" + "" + "" + "" + ""); + + QTest::newRow("invalid request (missing subscription id)") + << QStringLiteral("" + "" + "" + "" + "" + "AAAAAMAGAAAAAAAAAQ==" + "" + "" + "") + << (QStringList() + << QStringLiteral("AAAAAOQGAAAAAAAAAQ==" + "2006-08-22T01:00:50Z" + "" + "" + "")) + << static_cast(200) + << QStringLiteral("" + "" + "" + "" + "" + "" + "" + "" + "" + "Missing subscription id or watermark." + "ErrorInvalidPullSubscriptionId" + "0" + "" + "" + "" + "" + ""); + + QTest::newRow("invalid request (missing watermark)") + << QStringLiteral("" + "" + "" + "" + "f6bc657d-dde1-4f94-952d-143b95d6483d" + "" + "" + "" + "") + << (QStringList() + << QStringLiteral("AAAAAOQGAAAAAAAAAQ==" + "2006-08-22T01:00:50Z" + "" + "" + "")) + << static_cast(200) + << QStringLiteral("" + "" + "" + "" + "" + "" + "" + "" + "" + "Missing subscription id or watermark." + "ErrorInvalidPullSubscriptionId" + "0" + "" + "" + "" + "" + ""); + + QTest::newRow("valid request (no events)") + << QStringLiteral("" + "" + "" + "" + "f6bc657d-dde1-4f94-952d-143b95d6483d" + "AAAAAMAGAAAAAAAAAQ==" + "" + "" + "") + << QStringList() + << static_cast(200) + << QStringLiteral("" + "" + "" + "" + "" + "" + "" + "" + "" + "NoError" + "f6bc657d-dde1-4f94-952d-143b95d6483d" + "AAAAAMAGAAAAAAAAAQ==" + "false" + "" + "" + "" + "" + "" + ""); + + QTest::newRow("invalid request (not a GetEvents request)") + << QStringLiteral("" + "" + "" + "" + "f6bc657d-dde1-4f94-952d-143b95d6483d" + "30" + "" + "" + "") + << QStringList() + << static_cast(500) + << QString(); +} + +void UtEwsFakeSrvTest::getStreamingEventsRequest() +{ + bool callbackCalled = false; + const FakeEwsServer::DialogEntry::List emptyDialog; + + QString receivedReq; + QScopedPointer srv(new FakeEwsServer(this)); + QVERIFY(srv->start()); + QDateTime startTime = QDateTime::currentDateTime(); + + const QString event = QStringLiteral("" + "2006-08-22T01:00:10Z" + "" + "" + ""); + const QString content = QStringLiteral("" + "" + "" + "" + "f6bc657d-dde1-4f94-952d-143b95d6483d" + "1" + "" + "" + ""); + + srv->queueEventsXml(QStringList() << event); + + bool unknownRequestEncountered = false; + srv->setDefaultReplyCallback([&](const QString &req, QXmlResultItems &, const QXmlNamePool &) { + qDebug() << "Unknown EWS request encountered." << req; + unknownRequestEncountered = true; + return FakeEwsServer::EmptyResponse; + }); + + bool callbackError = false; + int responseNo = 0; + QDateTime pushEventTime; + auto resp = synchronousHttpReq(content, srv->portNumber(), [&](const QString &chunk) { + const QString respHead = QStringLiteral("" + "" + "" + "" + "" + "" + "" + "" + "" + "NoError" + "OK"); + const QString respTail = QStringLiteral("" + "" + "" + "" + ""); + const QString eventHead = QStringLiteral("" + "" + "f6bc657d-dde1-4f94-952d-143b95d6483d"); + const QString eventTail = QStringLiteral(""); + const QString event2 = QStringLiteral("" + "2006-08-22T01:00:50Z" + "" + "" + ""); + callbackCalled = true; + + QString expResp = respHead; + if (responseNo == 0) { + expResp += eventHead + event + eventTail; + } else if (responseNo == 2) { + expResp += eventHead + event2 + eventTail; + } + expResp += respTail; + + if (chunk != expResp) { + qWarning() << "Unexpected GetStreamingEventsResponse"; + callbackError = true; + return false; + } + + if (responseNo == 1) { + srv->queueEventsXml(QStringList() << event2); + pushEventTime = QDateTime::currentDateTime(); + } else if (responseNo == 2) { + /* Check if the response to the above event came "immediatelly" */ + QDateTime now = QDateTime::currentDateTime(); + if (pushEventTime.msecsTo(now) > 200) { + qWarning() << "Push event maximum roundtrip time exceeded"; + callbackError = true; + return false; + } + } + + responseNo++; + return true; + }); + + QCOMPARE(callbackCalled, true); + QCOMPARE(resp.second, static_cast(200)); + QCOMPARE(callbackError, false); + + QCOMPARE(unknownRequestEncountered, false); + + QDateTime now = QDateTime::currentDateTime(); + qint64 elapsed = startTime.msecsTo(now); + qDebug() << elapsed; + QVERIFY(elapsed >= 1 * 60 * 1000 - 600); + QVERIFY(elapsed <= 1 * 60 * 1000 + 600); +} + +void UtEwsFakeSrvTest::serverThread() +{ + const FakeEwsServer::DialogEntry::List dialog = { + { + QStringLiteral("if (//test1/a = ) then () else ()"), + FakeEwsServer::DialogEntry::ReplyCallback(), + QStringLiteral("Sample request 1") + } + }; + + QString receivedReq; + FakeEwsServerThread thread; + thread.start(); + QVERIFY(thread.waitServerStarted()); + thread.setDialog(dialog); + + auto resp = synchronousHttpReq(QStringLiteral(""), thread.portNumber()); + QCOMPARE(resp.first, QStringLiteral("")); + QCOMPARE(resp.second, static_cast(200)); + + thread.exit(); + thread.wait(); +} + +void UtEwsFakeSrvTest::delayedContentSize() +{ + /* This test case simulates the behaviour of KIO HTTP, which sends the data in three chunks: + * - initial headers + * - Content-Length header + end of headers + * - content + */ + + const FakeEwsServer::DialogEntry::List dialog = { + { + QStringLiteral("if (//test1/a = ) then () else ()"), + FakeEwsServer::DialogEntry::ReplyCallback(), + QStringLiteral("Sample request 1") + } + }; + + QString receivedReq; + FakeEwsServerThread thread; + thread.start(); + QVERIFY(thread.waitServerStarted()); + thread.setDialog(dialog); + + QTcpSocket sock; + sock.connectToHost(QHostAddress(QHostAddress::LocalHost), thread.portNumber()); + QVERIFY(sock.waitForConnected(1000)); + sock.write(QStringLiteral("POST /EWS/Exchange.asmx HTTP/1.1\r\n" + "Host: 127.0.0.1:%1\r\n" + "Connection: keep-alive\r\n" + "User-Agent: Mozilla/5.0 (X11; Linux x86_64) KHTML/5.26.0 (like Gecko) Konqueror/5.26\r\n" + "Pragma: no-cache\r\n" + "Cache-control: no-cache\r\n" + "Accept: text/html, text/*;q=0.9, image/jpeg;q=0.9, image/png;q=0.9, image/*;q=0.9, */*;q=0.8\r\n" + "Accept-Charset: utf-8,*;q=0.5\r\n" + "Accept-Language: pl-PL,en;q=0.9\r\n" + "Authorization: Basic dGVzdDp0ZXN0\r\n" + "Content-Type: text/xml\r\n").arg(thread.portNumber()).toAscii()); + sock.waitForBytesWritten(100); + QThread::msleep(100); + sock.write("Content-Length: 20\r\n\r\n"); + sock.waitForBytesWritten(100); + QThread::msleep(100); + sock.write(""); + sock.waitForBytesWritten(100); + sock.waitForReadyRead(300); + + QByteArray data = sock.readAll(); + + thread.exit(); + thread.wait(); + + QCOMPARE(data, QByteArray("HTTP/1.1 200 OK\r\nContent-Length: 4\r\nConnection: Keep-Alive\n\r\n")); +} + +void UtEwsFakeSrvTest::notAuthenticated() +{ + QScopedPointer srv(new FakeEwsServer(this)); + QVERIFY(srv->start()); + + QNetworkAccessManager nam(this); + QUrl url(QStringLiteral("http://127.0.0.1:%1/EWS/Exchange.asmx").arg(srv->portNumber())); + QNetworkRequest req(url); + req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("text/xml")); + QNetworkReply *reply = nam.post(req, "test"); + + QEventLoop loop; + QString resp; + ushort respCode = 0; + + connect(reply, &QNetworkReply::readyRead, this, [reply, &resp]() { + resp += QString::fromUtf8(reply->readAll()); + }); + connect(reply, &QNetworkReply::finished, this, [&loop, reply]() { + loop.exit(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toUInt()); + }); + respCode = loop.exec(); + + QCOMPARE(respCode, static_cast(401)); +} + +void UtEwsFakeSrvTest::badAuthentication() +{ + QScopedPointer srv(new FakeEwsServer(this)); + QVERIFY(srv->start()); + + QNetworkAccessManager nam(this); + QUrl url(QStringLiteral("http://127.0.0.1:%1/EWS/Exchange.asmx").arg(srv->portNumber())); + url.setUserName(QStringLiteral("foo")); + url.setPassword(QStringLiteral("bar")); + QNetworkRequest req(url); + req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("text/xml")); + QNetworkReply *reply = nam.post(req, "test"); + + QEventLoop loop; + QString resp; + ushort respCode = 0; + + connect(reply, &QNetworkReply::readyRead, this, [reply, &resp]() { + resp += QString::fromUtf8(reply->readAll()); + }); + connect(reply, &QNetworkReply::finished, this, [&loop, reply]() { + loop.exit(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toUInt()); + }); + respCode = loop.exec(); + + QCOMPARE(respCode, static_cast(401)); +} + +void UtEwsFakeSrvTest::xqueryResultsInCallback() +{ + bool callbackOk = false; + const FakeEwsServer::DialogEntry::List dialog = { + { + QStringLiteral("if (//test1/a = ) then (test) else ()"), + [&callbackOk](const QString &, QXmlResultItems & ri, const QXmlNamePool &) + { + if (ri.hasError()) { + qDebug() << "XQuery result has errors."; + return FakeEwsServer::EmptyResponse; + } + QXmlItem item = ri.next(); + if (item.isNull()) { + qDebug() << "XQuery result has no items."; + return FakeEwsServer::EmptyResponse; + } + if (!item.isNode()) { + qDebug() << "XQuery result is not a XML node."; + return FakeEwsServer::EmptyResponse; + } + QXmlNodeModelIndex index = item.toNodeModelIndex(); + if (index.isNull()) { + qDebug() << "XQuery XML node is NULL."; + return FakeEwsServer::EmptyResponse; + } + if (index.model()->stringValue(index) != QStringLiteral("test")) { + qDebug() << "Unexpected item value:" << index.model()->stringValue(index); + return FakeEwsServer::EmptyResponse; + } + return FakeEwsServer::DialogEntry::HttpResponse(QStringLiteral(""), 200); + }, + QStringLiteral("Sample request 1") + } + }; + + QString receivedReq; + QScopedPointer srv(new FakeEwsServer(this)); + srv->setDialog(dialog); + QVERIFY(srv->start()); + + auto resp = synchronousHttpReq(QStringLiteral(""), srv->portNumber()); + QCOMPARE(resp.first, QStringLiteral("")); + QCOMPARE(resp.second, static_cast(200)); +} + +QPair UtEwsFakeSrvTest::synchronousHttpReq(const QString &content, ushort port, + std::function chunkFn) +{ + QNetworkAccessManager nam(this); + QUrl url(QStringLiteral("http://127.0.0.1:%1/EWS/Exchange.asmx").arg(port)); + url.setUserName(QStringLiteral("test")); + url.setPassword(QStringLiteral("test")); + QNetworkRequest req(url); + req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("text/xml")); + QNetworkReply *reply = nam.post(req, content.toUtf8()); + + QEventLoop loop; + QString resp; + ushort respCode = 0; + + connect(reply, &QNetworkReply::readyRead, this, [reply, &resp, &chunkFn, &loop]() { + QString chunk = QString::fromUtf8(reply->readAll()); + if (chunkFn) { + bool cont = chunkFn(chunk); + if (!cont) { + reply->close(); + loop.exit(200); + return; + } + } else { + resp += chunk; + } + }); + connect(reply, &QNetworkReply::finished, this, [&loop, reply]() { + loop.exit(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toUInt()); + }); + respCode = loop.exec(); + + return {resp, respCode}; +} + +QTEST_MAIN(UtEwsFakeSrvTest) + +#include "ewsfakesrv_test.moc" diff --git a/resources/ews/test/isolatedtestbase.h b/resources/ews/test/isolatedtestbase.h new file mode 100644 --- /dev/null +++ b/resources/ews/test/isolatedtestbase.h @@ -0,0 +1,148 @@ +/* + 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 ISOLATEDTESTBASE_H +#define ISOLATEDTESTBASE_H + +#include +#include + +#include + +#include "fakeewsserver.h" + +namespace Akonadi +{ +class AgentInstance; +} +class FakeEwsServerThread; +class OrgKdeAkonadiEwsSettingsInterface; +class OrgKdeAkonadiEwsWalletInterface; + +class IsolatedTestBase : public QObject +{ + Q_OBJECT +public: + class Folder + { + public: + enum DistinguishedType { + None, + Root, + Inbox, + Outbox, + Sent, + Trash, + Drafts, + Templates, + Calendar, + Tasks, + Contacts + }; + + QString id; + QString name; + DistinguishedType type; + QString parentId; + }; + + typedef QVector FolderList; + + explicit IsolatedTestBase(QObject *parent = 0); + virtual ~IsolatedTestBase(); + + static QString loadResourceAsString(const QString &path); +protected: + virtual void init(); + virtual void cleanup(); + + bool setEwsResOnline(bool online, bool wait); +protected: + QString mEwsResIdentifier; + QString mAkonadiInstanceIdentifier; + QScopedPointer mFakeServerThread; + QScopedPointer mEwsInstance; + QScopedPointer mEwsSettingsInterface; + QScopedPointer mEwsWalletInterface; +}; + +class DialogEntryBase : public FakeEwsServer::DialogEntry +{ +public: + explicit DialogEntryBase(const QString &descr = QString(), const ReplyCallback &callback = ReplyCallback()) + { + replyCallback = callback; + description = descr; + }; +}; + +class MsgRootInboxDialogEntry : public DialogEntryBase +{ +public: + explicit MsgRootInboxDialogEntry(const QString &rootId, const QString &inboxId, + const QString &descr = QString(), + const ReplyCallback &callback = ReplyCallback()); +}; + +class SubscribedFoldersDialogEntry : public DialogEntryBase +{ +public: + explicit SubscribedFoldersDialogEntry(const IsolatedTestBase::FolderList &folders, + const QString &descr = QString(), + const ReplyCallback &callback = ReplyCallback()); +}; + +class SpecialFoldersDialogEntry : public DialogEntryBase +{ +public: + explicit SpecialFoldersDialogEntry(const IsolatedTestBase::FolderList &folders, + const QString &descr = QString(), + const ReplyCallback &callback = ReplyCallback()); +}; + +class GetTagsEmptyDialogEntry : public DialogEntryBase +{ +public: + explicit GetTagsEmptyDialogEntry(const QString &rootId, const QString &descr = QString(), + const ReplyCallback &callback = ReplyCallback()); +}; + +class SubscribeStreamingDialogEntry : public DialogEntryBase +{ +public: + explicit SubscribeStreamingDialogEntry(const QString &descr = QString(), + const ReplyCallback &callback = ReplyCallback()); +}; + +class SyncFolderHierInitialDialogEntry : public DialogEntryBase +{ +public: + explicit SyncFolderHierInitialDialogEntry(const IsolatedTestBase::FolderList &folders, + const QString &syncState, const QString &descr = QString(), + const ReplyCallback &callback = ReplyCallback()); +}; + +class UnsubscribeDialogEntry : public DialogEntryBase +{ +public: + explicit UnsubscribeDialogEntry(const QString &descr = QString(), + const ReplyCallback &callback = ReplyCallback()); +}; + +#endif diff --git a/resources/ews/test/isolatedtestbase.cpp b/resources/ews/test/isolatedtestbase.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/test/isolatedtestbase.cpp @@ -0,0 +1,292 @@ +/* + 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 "isolatedtestbase.h" + +#include + +#include +#include +#include + +#include "ewssettings.h" +#include "ewswallet.h" +#include "fakeewsserverthread.h" + +using namespace Akonadi; + +IsolatedTestBase::IsolatedTestBase(QObject *parent) + : QObject(parent), mFakeServerThread(new FakeEwsServerThread(this)) +{ + qsrand(QDateTime::currentDateTime().toTime_t()); +} + +IsolatedTestBase::~IsolatedTestBase() +{ +} + +void IsolatedTestBase::init() +{ + QVERIFY(Control::start()); + + /* Switch all resources offline to reduce interference from them */ + for (AgentInstance agent : AgentManager::self()->instances()) { + agent.setIsOnline(false); + } + + connect(AgentManager::self(), &AgentManager::instanceAdded, this, + [](const Akonadi::AgentInstance &instance) { + qDebug() << "AgentInstanceAdded" << instance.identifier(); + }); + + AgentType ewsType = AgentManager::self()->type(QStringLiteral("akonadi_ews_resource")); + AgentInstanceCreateJob *agentCreateJob = new AgentInstanceCreateJob(ewsType); + QVERIFY(agentCreateJob->exec()); + mEwsInstance.reset(new AgentInstance(agentCreateJob->instance())); + mEwsResIdentifier = mEwsInstance->identifier(); + mAkonadiInstanceIdentifier = QProcessEnvironment::systemEnvironment().value(QStringLiteral("AKONADI_INSTANCE")); + + mFakeServerThread->start(); + QVERIFY(mFakeServerThread->waitServerStarted()); + qDebug() << mFakeServerThread->portNumber(); + + mEwsSettingsInterface.reset(new OrgKdeAkonadiEwsSettingsInterface( + QStringLiteral("org.freedesktop.Akonadi.Resource.") + mEwsResIdentifier + + QStringLiteral(".") + mAkonadiInstanceIdentifier, + QStringLiteral("/Settings"), QDBusConnection::sessionBus(), this)); + QVERIFY(mEwsSettingsInterface->isValid()); + + mEwsWalletInterface.reset(new OrgKdeAkonadiEwsWalletInterface( + QStringLiteral("org.freedesktop.Akonadi.Resource.") + mEwsResIdentifier + + QStringLiteral(".") + mAkonadiInstanceIdentifier, + QStringLiteral("/Settings"), QDBusConnection::sessionBus(), this)); + QVERIFY(mEwsWalletInterface->isValid()); + + /* The EWS resource initializes its DBus adapters asynchronously. Therefore it can happen that + * due to a race access is attempted prior to their initialization. To fix this retry the DBus + * communication a few times before declaring failure. */ + QDBusReply reply; + int retryCnt = 4; + QString ewsUrl = QStringLiteral("http://127.0.0.1:%1/EWS/Exchange.asmx").arg(mFakeServerThread->portNumber()); + do { + mEwsSettingsInterface->setBaseUrl(ewsUrl); + reply = mEwsSettingsInterface->baseUrl(); + if (!reply.isValid()) { + qDebug() << "Failed to set base URL:" << reply.error().message(); + QThread::usleep(250); + } + } while (!reply.isValid() && retryCnt-- > 0); + QVERIFY(reply.isValid()); + QVERIFY(reply.value() == ewsUrl); + + mEwsSettingsInterface->setUsername(QStringLiteral("test")); + reply = mEwsSettingsInterface->username(); + QVERIFY(reply.isValid()); + QVERIFY(reply.value() == QStringLiteral("test")); + + mEwsWalletInterface->setTestPassword(QStringLiteral("test")); + AgentManager::self()->instance(mEwsResIdentifier).reconfigure(); +} + +void IsolatedTestBase::cleanup() +{ + mFakeServerThread->exit(); + mFakeServerThread->wait(); +} + +QString IsolatedTestBase::loadResourceAsString(const QString &path) +{ + QFile f(path); + if (f.open(QIODevice::ReadOnly)) { + return QString::fromUtf8(f.readAll()); + } + return QString(); +} + +bool IsolatedTestBase::setEwsResOnline(bool online, bool wait) +{ + if (wait) { + QEventLoop loop; + auto conn = connect(AgentManager::self(), &AgentManager::instanceOnline, this, + [&](const AgentInstance &instance, bool state) { + if (instance == *mEwsInstance && state == online) { + qDebug() << "is online" << state; + loop.exit(0); + } + }, Qt::QueuedConnection); + QTimer timer; + timer.setSingleShot(true); + connect(&timer, &QTimer::timeout, this, [&]() { + qDebug() << "Timeout waiting for desired resource online state."; + loop.exit(1); + }); + timer.start(3000); + mEwsInstance->setIsOnline(online); + bool status = (loop.exec() == 0); + disconnect(conn); + return status; + } else { + mEwsInstance->setIsOnline(online); + return true; + } +} + + +MsgRootInboxDialogEntry::MsgRootInboxDialogEntry(const QString &rootId, const QString &inboxId, + const QString &descr, const ReplyCallback &callback) + : DialogEntryBase(descr, callback) +{ + xQuery = IsolatedTestBase::loadResourceAsString(QStringLiteral(":/xquery/getfolder-inbox-msgroot")) + .arg(rootId).arg(inboxId); + description = QStringLiteral("GetFolder request for inbox and msgroot"); +} + +SubscribedFoldersDialogEntry::SubscribedFoldersDialogEntry(const IsolatedTestBase::FolderList &list, + const QString &descr, const ReplyCallback &callback) + : DialogEntryBase(descr, callback) +{ + static const QVector specialFolders = { + IsolatedTestBase::Folder::Inbox, + IsolatedTestBase::Folder::Calendar, + IsolatedTestBase::Folder::Tasks, + IsolatedTestBase::Folder::Contacts + }; + QHash folderHash; + for (const auto &folder : list) { + if (specialFolders.contains(folder.type)) { + folderHash.insert(folder.type, &folder); + } + } + + QString xml; + for (auto special : specialFolders) { + const IsolatedTestBase::Folder *folder = folderHash[special]; + if (QTest::qVerify(folder != nullptr, "folder != nullptr", "", __FILE__, __LINE__)) { + xml += QStringLiteral(""); + xml += QStringLiteral("NoError"); + xml += QStringLiteral(""); + xml += QStringLiteral("").arg(folder->id); + xml += QStringLiteral(""); + xml += QStringLiteral(""); + } + } + + xQuery = IsolatedTestBase::loadResourceAsString(QStringLiteral(":/xquery/getfolder-subscribedfolders")).arg(xml); +} + +SpecialFoldersDialogEntry::SpecialFoldersDialogEntry(const IsolatedTestBase::FolderList &list, + const QString &descr, const ReplyCallback &callback) + : DialogEntryBase(descr, callback) +{ + static const QVector specialFolders = { + IsolatedTestBase::Folder::Inbox, + IsolatedTestBase::Folder::Outbox, + IsolatedTestBase::Folder::Sent, + IsolatedTestBase::Folder::Trash, + IsolatedTestBase::Folder::Drafts + }; + QHash folderHash; + for (const auto &folder : list) { + if (specialFolders.contains(folder.type)) { + folderHash.insert(folder.type, &folder); + } + } + + QString xml; + for (auto special : specialFolders) { + const IsolatedTestBase::Folder *folder = folderHash[special]; + if (QTest::qVerify(folder != nullptr, "folder != nullptr", "", __FILE__, __LINE__)) { + xml += QStringLiteral(""); + xml += QStringLiteral("NoError"); + xml += QStringLiteral(""); + xml += QStringLiteral("").arg(folder->id); + xml += QStringLiteral(""); + xml += QStringLiteral(""); + } + } + + xQuery = IsolatedTestBase::loadResourceAsString(QStringLiteral(":/xquery/getfolder-specialfolders")).arg(xml); +} + +GetTagsEmptyDialogEntry::GetTagsEmptyDialogEntry(const QString &rootId, const QString &descr, + const ReplyCallback &callback) + : DialogEntryBase(descr, callback) +{ + xQuery = IsolatedTestBase::loadResourceAsString(QStringLiteral(":/xquery/getfolder-tags")).arg(rootId); +} + +SubscribeStreamingDialogEntry::SubscribeStreamingDialogEntry(const QString &descr, const ReplyCallback &callback) + : DialogEntryBase(descr, callback) +{ + xQuery = IsolatedTestBase::loadResourceAsString(QStringLiteral(":/xquery/subscribe-streaming")); +} + +SyncFolderHierInitialDialogEntry::SyncFolderHierInitialDialogEntry(const IsolatedTestBase::FolderList &list, + const QString &syncState, const QString &descr, const ReplyCallback &callback) + : DialogEntryBase(descr, callback) +{ + QHash childCount; + for (const auto &folder : list) { + ++childCount[folder.parentId]; + } + + QString xml; + for (const auto &folder : list) { + if (folder.type == IsolatedTestBase::Folder::Root) { + continue; + } + xml += QStringLiteral(""); + xml += QStringLiteral(""); + xml += QStringLiteral("").arg(folder.id); + xml += QStringLiteral("").arg(folder.parentId); + QString folderClass; + QString extraXml; + switch (folder.type) { + case IsolatedTestBase::Folder::Calendar: + folderClass = QStringLiteral("IPF.Calendar"); + break; + case IsolatedTestBase::Folder::Contacts: + folderClass = QStringLiteral("IPF.Contacts"); + break; + case IsolatedTestBase::Folder::Tasks: + folderClass = QStringLiteral("IPF.Tasks"); + break; + default: + folderClass = QStringLiteral("IPF.Note"); + extraXml = QStringLiteral("0"); + } + xml += QStringLiteral("%1").arg(folderClass); + xml += QStringLiteral("0"); + xml += QStringLiteral("%1").arg(folder.name); + xml += QStringLiteral("%1").arg(childCount[folder.id]); + xml += extraXml; + xml += QStringLiteral(""); + xml += QStringLiteral(""); + + } + xQuery = IsolatedTestBase::loadResourceAsString(QStringLiteral(":/xquery/syncfolderhierarhy-emptystate")) + .arg(syncState).arg(xml); +} + +UnsubscribeDialogEntry::UnsubscribeDialogEntry(const QString &descr, const ReplyCallback &callback) + : DialogEntryBase(descr, callback) +{ + xQuery = IsolatedTestBase::loadResourceAsString(QStringLiteral(":/xquery/unsubscribe")); +} + diff --git a/resources/ews/test/isolatedtestcommon.qrc b/resources/ews/test/isolatedtestcommon.qrc new file mode 100644 --- /dev/null +++ b/resources/ews/test/isolatedtestcommon.qrc @@ -0,0 +1,13 @@ + + + + resources/getfolder-inbox-msgroot.xq + resources/getfolder-specialfolders.xq + resources/getfolder-subscribedfolders.xq + resources/getfolder-tags.xq + resources/subscribe-streaming.xq + resources/syncfolderhierarhy-emptystate.xq + resources/unsubscribe.xq + + + \ No newline at end of file diff --git a/resources/ews/test/resources/getfolder-inbox-msgroot.xq b/resources/ews/test/resources/getfolder-inbox-msgroot.xq new file mode 100644 --- /dev/null +++ b/resources/ews/test/resources/getfolder-inbox-msgroot.xq @@ -0,0 +1,44 @@ +declare namespace t = "http://schemas.microsoft.com/exchange/services/2006/types"; +declare namespace m = "http://schemas.microsoft.com/exchange/services/2006/messages"; +declare namespace soap = "http://schemas.xmlsoap.org/soap/envelope/"; +if (/soap:Envelope/soap:Body/m:GetFolder and + //m:GetFolder/m:FolderShape/t:BaseShape = IdOnly and + //t:AdditionalProperties/t:FieldURI[@FieldURI="folder:DisplayName"] and + count(//t:AdditionalProperties/t:ExtendedFieldURI) = 2 and + //t:AdditionalProperties/t:ExtendedFieldURI[@PropertySetId="9bf757ae-69b5-4d8a-bf1d-2dd0c0871a28" and + @PropertyName="GlobalTags" and + @PropertyType="StringArray"] and + //t:AdditionalProperties/t:ExtendedFieldURI[@PropertySetId="9bf757ae-69b5-4d8a-bf1d-2dd0c0871a28" and + @PropertyName="GlobalTagsVersion" and + @PropertyType="Integer"] and + count(//m:GetFolder/m:FolderIds/t:DistinguishedFolderId) = 2 and + count(//m:GetFolder/m:FolderIds/t:FolderId) = 0 and + //m:GetFolder/m:FolderIds/t:DistinguishedFolderId[position()=1 and @Id="msgfolderroot"] and + //m:GetFolder/m:FolderIds/t:DistinguishedFolderId[position()=2 and @Id="inbox"] +) then ( + + + + + + + NoError + + + + Root + + + + + NoError + + + + Inbox + + + + + +) else () \ No newline at end of file diff --git a/resources/ews/test/resources/getfolder-specialfolders.xq b/resources/ews/test/resources/getfolder-specialfolders.xq new file mode 100644 --- /dev/null +++ b/resources/ews/test/resources/getfolder-specialfolders.xq @@ -0,0 +1,23 @@ +declare namespace t = "http://schemas.microsoft.com/exchange/services/2006/types"; +declare namespace m = "http://schemas.microsoft.com/exchange/services/2006/messages"; +declare namespace soap = "http://schemas.xmlsoap.org/soap/envelope/"; +if (/soap:Envelope/soap:Body/m:GetFolder and + //m:GetFolder/m:FolderShape/t:BaseShape = IdOnly and + not(//t:AdditionalProperties) and + count(//m:GetFolder/m:FolderIds/t:DistinguishedFolderId) = 5 and + count(//m:GetFolder/m:FolderIds/t:FolderId) = 0 and + //m:GetFolder/m:FolderIds/t:DistinguishedFolderId[position()=1 and @Id="inbox"] and + //m:GetFolder/m:FolderIds/t:DistinguishedFolderId[position()=2 and @Id="outbox"] and + //m:GetFolder/m:FolderIds/t:DistinguishedFolderId[position()=3 and @Id="sentitems"] and + //m:GetFolder/m:FolderIds/t:DistinguishedFolderId[position()=4 and @Id="deleteditems"] and + //m:GetFolder/m:FolderIds/t:DistinguishedFolderId[position()=5 and @Id="drafts"] +) then ( + + + + + + %1 + + +) else () \ No newline at end of file diff --git a/resources/ews/test/resources/getfolder-subscribedfolders.xq b/resources/ews/test/resources/getfolder-subscribedfolders.xq new file mode 100644 --- /dev/null +++ b/resources/ews/test/resources/getfolder-subscribedfolders.xq @@ -0,0 +1,22 @@ +declare namespace t = "http://schemas.microsoft.com/exchange/services/2006/types"; +declare namespace m = "http://schemas.microsoft.com/exchange/services/2006/messages"; +declare namespace soap = "http://schemas.xmlsoap.org/soap/envelope/"; +if (/soap:Envelope/soap:Body/m:GetFolder and + //m:GetFolder/m:FolderShape/t:BaseShape = IdOnly and + not(//t:AdditionalProperties) and + count(//m:GetFolder/m:FolderIds/t:DistinguishedFolderId) = 4 and + count(//m:GetFolder/m:FolderIds/t:FolderId) = 0 and + //m:GetFolder/m:FolderIds/t:DistinguishedFolderId[position()=1 and @Id="inbox"] and + //m:GetFolder/m:FolderIds/t:DistinguishedFolderId[position()=2 and @Id="calendar"] and + //m:GetFolder/m:FolderIds/t:DistinguishedFolderId[position()=3 and @Id="tasks"] and + //m:GetFolder/m:FolderIds/t:DistinguishedFolderId[position()=4 and @Id="contacts"] +) then ( + + + + + + %1 + + +) else () \ No newline at end of file diff --git a/resources/ews/test/resources/getfolder-tags.xq b/resources/ews/test/resources/getfolder-tags.xq new file mode 100644 --- /dev/null +++ b/resources/ews/test/resources/getfolder-tags.xq @@ -0,0 +1,32 @@ +declare namespace t = "http://schemas.microsoft.com/exchange/services/2006/types"; +declare namespace m = "http://schemas.microsoft.com/exchange/services/2006/messages"; +declare namespace soap = "http://schemas.xmlsoap.org/soap/envelope/"; +if (/soap:Envelope/soap:Body/m:GetFolder and + //m:GetFolder/m:FolderShape/t:BaseShape = IdOnly and + count(//t:AdditionalProperties/t:ExtendedFieldURI) = 2 and + //t:AdditionalProperties/t:ExtendedFieldURI[@PropertySetId="9bf757ae-69b5-4d8a-bf1d-2dd0c0871a28" and + @PropertyName="GlobalTags" and + @PropertyType="StringArray"] and + //t:AdditionalProperties/t:ExtendedFieldURI[@PropertySetId="9bf757ae-69b5-4d8a-bf1d-2dd0c0871a28" and + @PropertyName="GlobalTagsVersion" and + @PropertyType="Integer"] and + count(//m:GetFolder/m:FolderIds/t:DistinguishedFolderId) = 1 and + count(//m:GetFolder/m:FolderIds/t:FolderId) = 0 and + //m:GetFolder/m:FolderIds/t:DistinguishedFolderId[@Id="msgfolderroot"] +) then ( + + + + + + + NoError + + + + + + + + +) else () \ No newline at end of file diff --git a/resources/ews/test/resources/subscribe-streaming.xq b/resources/ews/test/resources/subscribe-streaming.xq new file mode 100644 --- /dev/null +++ b/resources/ews/test/resources/subscribe-streaming.xq @@ -0,0 +1,23 @@ +declare namespace t = "http://schemas.microsoft.com/exchange/services/2006/types"; +declare namespace m = "http://schemas.microsoft.com/exchange/services/2006/messages"; +declare namespace soap = "http://schemas.xmlsoap.org/soap/envelope/"; +if (/soap:Envelope/soap:Body/m:Subscribe/m:StreamingSubscriptionRequest and + count(//m:StreamingSubscriptionRequest/t:FolderIds/t:FolderId) = 4 and + //m:StreamingSubscriptionRequest/t:FolderIds/t:FolderId[@Id="aW5ib3g="] and + //m:StreamingSubscriptionRequest/t:FolderIds/t:FolderId[@Id="Y2FsZW5kYXI="] and + //m:StreamingSubscriptionRequest/t:FolderIds/t:FolderId[@Id="dGFza3M="] and + //m:StreamingSubscriptionRequest/t:FolderIds/t:FolderId[@Id="Y29udGFjdHM="] and + count(//m:StreamingSubscriptionRequest/t:EventTypes/t:EventType) = 6 +) then ( + + + + " + " + + NoError + otPO1iOPzS+vL5HOuTZY+cCfIIn+vRqOO0ZHTEWAa8k= + + + +) else () \ No newline at end of file diff --git a/resources/ews/test/resources/syncfolderhierarhy-emptystate.xq b/resources/ews/test/resources/syncfolderhierarhy-emptystate.xq new file mode 100644 --- /dev/null +++ b/resources/ews/test/resources/syncfolderhierarhy-emptystate.xq @@ -0,0 +1,31 @@ +declare namespace t = "http://schemas.microsoft.com/exchange/services/2006/types"; +declare namespace m = "http://schemas.microsoft.com/exchange/services/2006/messages"; +declare namespace soap = "http://schemas.xmlsoap.org/soap/envelope/"; +if (/soap:Envelope/soap:Body/m:SyncFolderHierarchy and + //m:SyncFolderHierarchy/m:FolderShape/t:BaseShape = Default and + count(//t:AdditionalProperties/t:ExtendedFieldURI) = 1 and + //t:AdditionalProperties/t:ExtendedFieldURI[@PropertyTag="0x3613" and + @PropertyType="String"] and + count(//t:AdditionalProperties/t:FieldURI) = 2 and + //t:AdditionalProperties/t:FieldURI[@FieldURI="folder:EffectiveRights"] and + //t:AdditionalProperties/t:FieldURI[@FieldURI="folder:ParentFolderId"] and + count(//m:SyncFolderHierarchy/m:SyncFolderId/t:DistinguishedFolderId) = 1 and + count(//m:SyncFolderHierarchy/m:SyncFolderId/t:FolderId) = 0 and + //m:SyncFolderHierarchy/m:SyncFolderId/t:DistinguishedFolderId[@Id="msgfolderroot"] +) then ( + + + + + + + NoError + %1 + true + + %2 + + + + +) else () \ No newline at end of file diff --git a/resources/ews/test/resources/unsubscribe.xq b/resources/ews/test/resources/unsubscribe.xq new file mode 100644 --- /dev/null +++ b/resources/ews/test/resources/unsubscribe.xq @@ -0,0 +1,16 @@ +declare namespace t = "http://schemas.microsoft.com/exchange/services/2006/types"; +declare namespace m = "http://schemas.microsoft.com/exchange/services/2006/messages"; +declare namespace soap = "http://schemas.xmlsoap.org/soap/envelope/"; +if (/soap:Envelope/soap:Body/m:Unsubscribe/m:SubscriptionId = otPO1iOPzS+vL5HOuTZY+cCfIIn+vRqOO0ZHTEWAa8k= +) then ( + + + + " + " + + NoError + + + +) else () \ No newline at end of file diff --git a/resources/ews/test/statemonitor.h b/resources/ews/test/statemonitor.h new file mode 100644 --- /dev/null +++ b/resources/ews/test/statemonitor.h @@ -0,0 +1,97 @@ +/* + 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 + +class StateMonitorBase : public QObject +{ + Q_OBJECT +public: + explicit StateMonitorBase(QObject *parent) : QObject(parent) {}; + virtual ~StateMonitorBase() = 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); + ~CollectionStateMonitor() = default; + Akonadi::Monitor &monitor() + { + return mMonitor; + }; +private: + void stateChanged(const Akonadi::Collection &col); + + Akonadi::Monitor mMonitor; + QSet mPending; + const QHash &mStateHash; + const StateComparisonFunc &mComparisonFunc; + const QString &mInboxId; +}; + +template +CollectionStateMonitor::CollectionStateMonitor(QObject *parent, const QHash &stateHash, + const QString &inboxId, const StateComparisonFunc &comparisonFunc) + : StateMonitorBase(parent), mMonitor(this), mPending(stateHash.keys().toSet()), mStateHash(stateHash), + mComparisonFunc(comparisonFunc), mInboxId(inboxId) +{ + 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); + }); +} + +template +void CollectionStateMonitor::stateChanged(const Akonadi::Collection &col) +{ + auto remoteId = col.remoteId() == QStringLiteral("INBOX") ? mInboxId : 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(); + } +} + +#endif diff --git a/resources/ews/test/statemonitor.cpp b/resources/ews/test/statemonitor.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/test/statemonitor.cpp @@ -0,0 +1,21 @@ +/* + 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 "statemonitor.h" + diff --git a/resources/ews/test/testenv/config.xml.cmake b/resources/ews/test/testenv/config.xml.cmake new file mode 100644 --- /dev/null +++ b/resources/ews/test/testenv/config.xml.cmake @@ -0,0 +1,8 @@ + + kdehome + xdgconfig + xdglocal + true + ${CMAKE_CURRENT_BINARY_DIR}/../:$ENV{PATH} + ${EWS_TMP_XDG_DATA_DIR}:/usr/local/share:/usr/share + diff --git a/resources/ews/test/testenv/kdehome/share/apps/.keep b/resources/ews/test/testenv/kdehome/share/apps/.keep new file mode 100644 --- /dev/null +++ b/resources/ews/test/testenv/kdehome/share/apps/.keep @@ -0,0 +1 @@ +force git to include this file diff --git a/resources/ews/test/testenv/kdehome/share/config/akonadi-firstrunrc b/resources/ews/test/testenv/kdehome/share/config/akonadi-firstrunrc new file mode 100644 --- /dev/null +++ b/resources/ews/test/testenv/kdehome/share/config/akonadi-firstrunrc @@ -0,0 +1,3 @@ +[ProcessedDefaults] +defaultaddressbook=done +defaultcalendar=done diff --git a/resources/ews/test/testenv/kdehome/share/config/kdebugrc b/resources/ews/test/testenv/kdehome/share/config/kdebugrc new file mode 100755 --- /dev/null +++ b/resources/ews/test/testenv/kdehome/share/config/kdebugrc @@ -0,0 +1,103 @@ +[0] +AbortFatal=true +ErrorFilename[$e]=kdebug.dbg +ErrorOutput=2 +FatalFilename[$e]=kdebug.dbg +FatalOutput=2 +InfoFilename[$e]=kdebug.dbg +InfoOutput=2 +WarnFilename[$e]=kdebug.dbg +WarnOutput=2 + +[kbuildsycoca4] +AbortFatal=true +ErrorOutput=0 +FatalOutput=0 +InfoOutput=0 +WarnOutput=0 + +[kdecore (services)] +AbortFatal=true +ErrorOutput=0 +FatalOutput=0 +InfoOutput=0 +WarnOutput=0 + +[264] +AbortFatal=true +ErrorFilename[$e]=kdebug.dbg +ErrorOutput=4 +FatalFilename[$e]=kdebug.dbg +FatalOutput=4 +InfoFilename[$e]=kdebug.dbg +WarnFilename[$e]=kdebug.dbg +WarnOutput=4 + +[5250] +InfoOutput=2 + +[7009] +AbortFatal=true +ErrorFilename[$e]=kdebug.dbg +ErrorOutput=4 +FatalFilename[$e]=kdebug.dbg +FatalOutput=4 +InfoFilename[$e]=kdebug.dbg +InfoOutput=4 +WarnFilename[$e]=kdebug.dbg +WarnOutput=4 + +[7011] +AbortFatal=true +ErrorFilename[$e]=kdebug.dbg +ErrorOutput=4 +FatalFilename[$e]=kdebug.dbg +FatalOutput=4 +InfoFilename[$e]=kdebug.dbg +InfoOutput=4 +WarnFilename[$e]=kdebug.dbg +WarnOutput=4 + +[7012] +AbortFatal=true +ErrorFilename[$e]=kdebug.dbg +ErrorOutput=4 +FatalFilename[$e]=kdebug.dbg +FatalOutput=4 +InfoFilename[$e]=kdebug.dbg +InfoOutput=4 +WarnFilename[$e]=kdebug.dbg +WarnOutput=4 + +[7014] +AbortFatal=true +ErrorFilename[$e]=kdebug.dbg +ErrorOutput=0 +FatalFilename[$e]=kdebug.dbg +FatalOutput=0 +InfoFilename[$e]=kdebug.dbg +InfoOutput=0 +WarnFilename[$e]=kdebug.dbg +WarnOutput=0 + +[7021] +AbortFatal=true +ErrorFilename[$e]=kdebug.dbg +ErrorOutput=4 +FatalFilename[$e]=kdebug.dbg +FatalOutput=4 +InfoFilename[$e]=kdebug.dbg +InfoOutput=4 +WarnFilename[$e]=kdebug.dbg +WarnOutput=4 + +[7105] +AbortFatal=true +ErrorFilename[$e]=kdebug.dbg +ErrorOutput=2 +FatalFilename[$e]=kdebug.dbg +FatalOutput=2 +InfoFilename[$e]=kdebug.dbg +InfoOutput=2 +WarnFilename[$e]=kdebug.dbg +WarnOutput=2 diff --git a/resources/ews/test/testenv/xdgconfig/akonadi/akonadiserverrc b/resources/ews/test/testenv/xdgconfig/akonadi/akonadiserverrc new file mode 100644 --- /dev/null +++ b/resources/ews/test/testenv/xdgconfig/akonadi/akonadiserverrc @@ -0,0 +1,4 @@ +[%General] + +[Search] +Manager=Dummy diff --git a/resources/ews/test/testenv/xdglocal/.keep b/resources/ews/test/testenv/xdglocal/.keep new file mode 100644 --- /dev/null +++ b/resources/ews/test/testenv/xdglocal/.keep @@ -0,0 +1 @@ +force git to include this file diff --git a/resources/ews/test/unittests/CMakeLists.txt b/resources/ews/test/unittests/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/resources/ews/test/unittests/CMakeLists.txt @@ -0,0 +1,36 @@ +# +# 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. + +kde_enable_exceptions() + +add_library(uttesthelpers STATIC faketransferjob.cpp) +target_link_libraries(uttesthelpers Qt5::Core KF5::KIOCore) + +macro(akonadi_ews_add_ut utname) + add_executable(${utname} ${utname}.cpp) + target_link_libraries(${utname} Qt5::Test uttesthelpers ewsclient) + add_test(NAME ${utname} COMMAND ${utname}) +endmacro(akonadi_ews_add_ut utname) + +akonadi_ews_add_ut(ewsmoveitemrequest_ut) +akonadi_ews_add_ut(ewsdeleteitemrequest_ut) +akonadi_ews_add_ut(ewsgetitemrequest_ut) +akonadi_ews_add_ut(ewsunsubscriberequest_ut) +akonadi_ews_add_ut(ewsattachment_ut) + + diff --git a/resources/ews/test/unittests/ewsattachment_ut.cpp b/resources/ews/test/unittests/ewsattachment_ut.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/test/unittests/ewsattachment_ut.cpp @@ -0,0 +1,946 @@ +/* + 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 +#include + +#include "ewsattachment.h" +#include "fakehttppost.h" + +class UtEwsAttachment : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void read(); + void read_data(); + void write(); + void write_data(); +}; + +Q_DECLARE_METATYPE(EwsAttachment::Type) + +static const QString xmlMsgNsUri = QStringLiteral("http://schemas.microsoft.com/exchange/services/2006/message"); +static const QString xmlTypeNsUri = QStringLiteral("http://schemas.microsoft.com/exchange/services/2006/types"); + +static const QString xmlHead = QStringLiteral(""); +static const QString xmlDocHead = xmlHead + QStringLiteral(""); +static const QString xmlDocTail = QStringLiteral(""); +static const QString xmlItemAttHead = xmlDocHead + QStringLiteral(""); +static const QString xmlItemAttTail = QStringLiteral("") + xmlDocTail; +static const QString xmlFileAttHead = xmlDocHead + QStringLiteral(""); +static const QString xmlFileAttTail = QStringLiteral("") + xmlDocTail; + +void UtEwsAttachment::read() +{ + QFETCH(QString, xml); + QFETCH(bool, isValid); + + QXmlStreamReader reader(xml); + + reader.readNextStartElement(); + reader.readNextStartElement(); + + EwsAttachment att(reader); + + QCOMPARE(att.isValid(), isValid); + + if (!isValid) { + return; + } + + QFETCH(EwsAttachment::Type, type); + + QCOMPARE(att.type(), type); + + QFETCH(bool, hasId); + QFETCH(QString, id); + QCOMPARE(att.hasId(), hasId); + if (hasId) { + QCOMPARE(att.id(), id); + } + + QFETCH(bool, hasName); + QFETCH(QString, name); + QCOMPARE(att.hasName(), hasName); + if (hasName) { + QCOMPARE(att.name(), name); + } + + QFETCH(bool, hasContentType); + QFETCH(QString, contentType); + QCOMPARE(att.hasContentType(), hasContentType); + if (hasContentType) { + QCOMPARE(att.contentType(), contentType); + } + + QFETCH(bool, hasContentId); + QFETCH(QString, contentId); + QCOMPARE(att.hasContentId(), hasContentId); + if (hasContentId) { + QCOMPARE(att.contentId(), contentId); + } + + QFETCH(bool, hasContentLocation); + QFETCH(QString, contentLocation); + QCOMPARE(att.hasContentLocation(), hasContentLocation); + if (hasContentLocation) { + QCOMPARE(att.contentLocation(), contentLocation); + } + + QFETCH(bool, hasSize); + QFETCH(long, size); + QCOMPARE(att.hasSize(), hasSize); + if (hasSize) { + QCOMPARE(att.size(), size); + } + + QFETCH(bool, hasLastModifiedTime); + QFETCH(QDateTime, lastModifiedTime); + QCOMPARE(att.hasLastModifiedTime(), hasLastModifiedTime); + if (hasLastModifiedTime) { + QCOMPARE(att.lastModifiedTime(), lastModifiedTime); + } + + QFETCH(bool, hasIsInline); + QFETCH(bool, isInline); + QCOMPARE(att.hasIsInline(), hasIsInline); + if (hasIsInline) { + QCOMPARE(att.isInline(), isInline); + } + + QFETCH(bool, hasIsContactPhoto); + QFETCH(bool, isContactPhoto); + QCOMPARE(att.hasIsContactPhoto(), hasIsContactPhoto); + if (hasIsContactPhoto) { + QCOMPARE(att.isContactPhoto(), isContactPhoto); + } + + QFETCH(bool, hasContent); + QFETCH(QByteArray, content); + QCOMPARE(att.hasContent(), hasContent); + if (hasContent) { + QCOMPARE(att.content(), content); + } + + QFETCH(bool, hasItem); + QFETCH(EwsItem, item); + QCOMPARE(att.hasItem(), hasItem); + if (hasItem) { + QCOMPARE(att.item(), item); + } + reader.skipCurrentElement(); + + reader.readNextStartElement(); + QCOMPARE(reader.name().toString(), QStringLiteral("Test2")); +} + +void UtEwsAttachment::read_data() +{ + QTest::addColumn("xml"); + QTest::addColumn("isValid"); + + QTest::addColumn("type"); + + QTest::addColumn("hasId"); + QTest::addColumn("id"); + + QTest::addColumn("hasName"); + QTest::addColumn("name"); + + QTest::addColumn("hasContentType"); + QTest::addColumn("contentType"); + + QTest::addColumn("hasContentId"); + QTest::addColumn("contentId"); + + QTest::addColumn("hasContentLocation"); + QTest::addColumn("contentLocation"); + + QTest::addColumn("hasSize"); + QTest::addColumn("size"); + + QTest::addColumn("hasLastModifiedTime"); + QTest::addColumn("lastModifiedTime"); + + QTest::addColumn("hasIsInline"); + QTest::addColumn("isInline"); + + QTest::addColumn("hasIsContactPhoto"); + QTest::addColumn("isContactPhoto"); + + QTest::addColumn("hasContent"); + QTest::addColumn("content"); + + QTest::addColumn("hasItem"); + QTest::addColumn("item"); + + QTest::newRow("invalid namespace") + << xmlDocHead + QStringLiteral("") + xmlDocTail + << false + << EwsAttachment::UnknownAttachment + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << 0l + << false << QDateTime() + << false << false + << false << false + << false << QByteArray() + << false << EwsItem(); + + QTest::newRow("invalid type") + << xmlDocHead + QStringLiteral("") + xmlDocTail + << false + << EwsAttachment::UnknownAttachment + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << 0l + << false << QDateTime() + << false << false + << false << false + << false << QByteArray() + << false << EwsItem(); + + QTest::newRow("empty file attachment") + << xmlDocHead + QStringLiteral("") + xmlDocTail + << true + << EwsAttachment::FileAttachment + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << 0l + << false << QDateTime() + << false << false + << false << false + << false << QByteArray() + << false << EwsItem(); + + QTest::newRow("empty item attachment") + << xmlDocHead + QStringLiteral("") + xmlDocTail + << true + << EwsAttachment::ItemAttachment + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << 0l + << false << QDateTime() + << false << false + << false << false + << false << QByteArray() + << false << EwsItem(); + + QTest::newRow("empty reference attachment") + << xmlDocHead + QStringLiteral("") + xmlDocTail + << true + << EwsAttachment::ReferenceAttachment + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << 0l + << false << QDateTime() + << false << false + << false << false + << false << QByteArray() + << false << EwsItem(); + + QTest::newRow("invalid attachment id - bad namespace") + << xmlFileAttHead + QStringLiteral("") + xmlFileAttTail + << false + << EwsAttachment::UnknownAttachment + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << 0l + << false << QDateTime() + << false << false + << false << false + << false << QByteArray() + << false << EwsItem(); + + QTest::newRow("invalid attachment id - bad attribute") + << xmlFileAttHead + QStringLiteral("") + xmlFileAttTail + << false + << EwsAttachment::UnknownAttachment + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << 0l + << false << QDateTime() + << false << false + << false << false + << false << QByteArray() + << false << EwsItem(); + + QTest::newRow("valid attachment id") + << xmlFileAttHead + QStringLiteral("") + xmlFileAttTail + << true + << EwsAttachment::FileAttachment + << true << QStringLiteral("JCPhyc4Kg73tIuurR3c0Pw==") + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << 0l + << false << QDateTime() + << false << false + << false << false + << false << QByteArray() + << false << EwsItem(); + + QTest::newRow("invalid name") + << xmlFileAttHead + QStringLiteral("Test name") + xmlFileAttTail + << false + << EwsAttachment::UnknownAttachment + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << 0l + << false << QDateTime() + << false << false + << false << false + << false << QByteArray() + << false << EwsItem(); + + QTest::newRow("valid name") + << xmlFileAttHead + QStringLiteral("Test name") + xmlFileAttTail + << true + << EwsAttachment::FileAttachment + << false << QString() + << true << QStringLiteral("Test name") + << false << QString() + << false << QString() + << false << QString() + << false << 0l + << false << QDateTime() + << false << false + << false << false + << false << QByteArray() + << false << EwsItem(); + + QTest::newRow("valid content type") + << xmlFileAttHead + QStringLiteral("application/x-test") + xmlFileAttTail + << true + << EwsAttachment::FileAttachment + << false << QString() + << false << QString() + << true << QStringLiteral("application/x-test") + << false << QString() + << false << QString() + << false << 0l + << false << QDateTime() + << false << false + << false << false + << false << QByteArray() + << false << EwsItem(); + + QTest::newRow("valid content id") + << xmlFileAttHead + QStringLiteral("FE938BD618330B9DA0C965A6077BB3FF20415531@1") + xmlFileAttTail + << true + << EwsAttachment::FileAttachment + << false << QString() + << false << QString() + << false << QString() + << true << QStringLiteral("FE938BD618330B9DA0C965A6077BB3FF20415531@1") + << false << QString() + << false << 0l + << false << QDateTime() + << false << false + << false << false + << false << QByteArray() + << false << EwsItem(); + + QTest::newRow("valid content location") + << xmlFileAttHead + QStringLiteral("file:///foo/bar.txt") + xmlFileAttTail + << true + << EwsAttachment::FileAttachment + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << true << QStringLiteral("file:///foo/bar.txt") + << false << 0l + << false << QDateTime() + << false << false + << false << false + << false << QByteArray() + << false << EwsItem(); + + QTest::newRow("invalid size - not a number") + << xmlFileAttHead + QStringLiteral("foo") + xmlFileAttTail + << false + << EwsAttachment::UnknownAttachment + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << 0l + << false << QDateTime() + << false << false + << false << false + << false << QByteArray() + << false << EwsItem(); + + QTest::newRow("valid size") + << xmlFileAttHead + QStringLiteral("123") + xmlFileAttTail + << true + << EwsAttachment::FileAttachment + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << true << 123l + << false << QDateTime() + << false << false + << false << false + << false << QByteArray() + << false << EwsItem(); + + QTest::newRow("invalid last modified time - bad time") + << xmlFileAttHead + QStringLiteral("2017-01-03T09:74:39") + xmlFileAttTail + << false + << EwsAttachment::UnknownAttachment + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << 0l + << false << QDateTime() + << false << false + << false << false + << false << QByteArray() + << false << EwsItem(); + + QTest::newRow("invalid last modified time - nested XML element") + << xmlFileAttHead + QStringLiteral("2017-01-03T09:74:39") + xmlFileAttTail + << false + << EwsAttachment::UnknownAttachment + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << 0l + << false << QDateTime() + << false << false + << false << false + << false << QByteArray() + << false << EwsItem(); + + QTest::newRow("valid last modified time") + << xmlFileAttHead + QStringLiteral("2017-01-03T09:24:39") + xmlFileAttTail + << true + << EwsAttachment::FileAttachment + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << 0l + << true << QDateTime::fromTime_t(1483431879) + << false << false + << false << false + << false << QByteArray() + << false << EwsItem(); + + QTest::newRow("invalid is inline - bad value") + << xmlFileAttHead + QStringLiteral("fake") + xmlFileAttTail + << false + << EwsAttachment::UnknownAttachment + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << 0l + << false << QDateTime() + << false << false + << false << false + << false << QByteArray() + << false << EwsItem(); + + QTest::newRow("valid is inline") + << xmlFileAttHead + QStringLiteral("true") + xmlFileAttTail + << true + << EwsAttachment::FileAttachment + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << 0l + << false << QDateTime() + << true << true + << false << false + << false << QByteArray() + << false << EwsItem(); + + QTest::newRow("invalid is contact photo - bad value") + << xmlFileAttHead + QStringLiteral("fake") + xmlFileAttTail + << false + << EwsAttachment::UnknownAttachment + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << 0l + << false << QDateTime() + << false << false + << false << false + << false << QByteArray() + << false << EwsItem(); + + QTest::newRow("invalid is contact photo - inside ItemAttachment") + << xmlItemAttHead + QStringLiteral("true") + xmlItemAttTail + << false + << EwsAttachment::UnknownAttachment + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << 0l + << false << QDateTime() + << false << false + << false << false + << false << QByteArray() + << false << EwsItem(); + + QTest::newRow("valid is contact photo") + << xmlFileAttHead + QStringLiteral("true") + xmlFileAttTail + << true + << EwsAttachment::FileAttachment + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << 0l + << false << QDateTime() + << false << false + << true << true + << false << QByteArray() + << false << EwsItem(); + + QTest::newRow("invalid content - nested XML element") + << xmlFileAttHead + QStringLiteral("djsaoijsa") + xmlFileAttTail + << false + << EwsAttachment::UnknownAttachment + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << 0l + << false << QDateTime() + << false << false + << false << false + << false << QByteArray() + << false << EwsItem(); + + QTest::newRow("invalid content - inside ItemAttachment") + << xmlItemAttHead + QStringLiteral("VGhpcyBpcyBhIHRlc3Q=") + xmlItemAttTail + << false + << EwsAttachment::UnknownAttachment + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << 0l + << false << QDateTime() + << false << false + << false << false + << false << QByteArray() + << false << EwsItem(); + + QTest::newRow("valid content") + << xmlFileAttHead + QStringLiteral("VGhpcyBpcyBhIHRlc3Q=") + xmlFileAttTail + << true + << EwsAttachment::FileAttachment + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << 0l + << false << QDateTime() + << false << false + << false << false + << true << QByteArray("This is a test") + << false << EwsItem(); + + QTest::newRow("invalid item - bad data") + << xmlItemAttHead + QStringLiteral("") + xmlItemAttTail + << false + << EwsAttachment::UnknownAttachment + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << 0l + << false << QDateTime() + << false << false + << false << false + << false << QByteArray() + << false << EwsItem(); + + QTest::newRow("invalid item - inside FileAttachment") + << xmlFileAttHead + QStringLiteral("") + xmlFileAttTail + << false + << EwsAttachment::UnknownAttachment + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << 0l + << false << QDateTime() + << false << false + << false << false + << false << QByteArray() + << false << EwsItem(); + + EwsItem item1; + item1.setType(EwsItemTypeItem); + item1.setField(EwsItemFieldItemId, QVariant::fromValue(EwsId(QStringLiteral("VGhpcyBpcyBhIHRlc3Q="), QStringLiteral("muKls0n8pUM=")))); + QTest::newRow("valid item") + << xmlItemAttHead + QStringLiteral("") + xmlItemAttTail + << true + << EwsAttachment::ItemAttachment + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << 0l + << false << QDateTime() + << false << false + << false << false + << false << QByteArray() + << true << item1; +} + +void UtEwsAttachment::write() +{ + QFETCH(EwsAttachment::Type, type); + QFETCH(bool, isValid); + + EwsAttachment att; + if (isValid) { + att.setType(type); + + QFETCH(bool, hasId); + QFETCH(QString, id); + if (hasId) { + att.setId(id); + } + + QFETCH(bool, hasName); + QFETCH(QString, name); + if (hasName) { + att.setName(name); + } + + QFETCH(bool, hasContentType); + QFETCH(QString, contentType); + if (hasContentType) { + att.setContentType(contentType); + } + + QFETCH(bool, hasContentId); + QFETCH(QString, contentId); + if (hasContentId) { + att.setContentId(contentId); + } + + QFETCH(bool, hasContentLocation); + QFETCH(QString, contentLocation); + if (hasContentLocation) { + att.setContentLocation(contentLocation); + } + + QFETCH(bool, hasSize); + QFETCH(long, size); + if (hasSize) { + att.setSize(size); + } + + QFETCH(bool, hasLastModifiedTime); + QFETCH(QDateTime, lastModifiedTime); + if (hasLastModifiedTime) { + att.setLastModifiedTime(lastModifiedTime); + } + + QFETCH(bool, hasIsInline); + QFETCH(bool, isInline); + if (hasIsInline) { + att.setIsInline(isInline); + } + + QFETCH(bool, hasIsContactPhoto); + QFETCH(bool, isContactPhoto); + if (hasIsContactPhoto) { + att.setIsContactPhoto(isContactPhoto); + } + + QFETCH(bool, hasContent); + QFETCH(QByteArray, content); + if (hasContent) { + att.setContent(content); + } + + QFETCH(bool, hasItem); + QFETCH(EwsItem, item); + if (hasItem) { + att.setItem(item); + } + } + + + QFETCH(QString, xml); + + QString xmlData; + QXmlStreamWriter writer(&xmlData); + + writer.setCodec("UTF-8"); + + writer.writeStartDocument(); + + writer.writeDefaultNamespace(xmlTypeNsUri); + + att.write(writer); + + writer.writeEndDocument(); + + QCOMPARE(xmlData.trimmed(), xml); +} + +void UtEwsAttachment::write_data() +{ + QTest::addColumn("xml"); + QTest::addColumn("isValid"); + + QTest::addColumn("type"); + + QTest::addColumn("hasId"); + QTest::addColumn("id"); + + QTest::addColumn("hasName"); + QTest::addColumn("name"); + + QTest::addColumn("hasContentType"); + QTest::addColumn("contentType"); + + QTest::addColumn("hasContentId"); + QTest::addColumn("contentId"); + + QTest::addColumn("hasContentLocation"); + QTest::addColumn("contentLocation"); + + QTest::addColumn("hasSize"); + QTest::addColumn("size"); + + QTest::addColumn("hasLastModifiedTime"); + QTest::addColumn("lastModifiedTime"); + + QTest::addColumn("hasIsInline"); + QTest::addColumn("isInline"); + + QTest::addColumn("hasIsContactPhoto"); + QTest::addColumn("isContactPhoto"); + + QTest::addColumn("hasContent"); + QTest::addColumn("content"); + + QTest::addColumn("hasItem"); + QTest::addColumn("item"); + + QTest::newRow("invalid") + << xmlHead + << false + << EwsAttachment::UnknownAttachment + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << 0l + << false << QDateTime() + << false << false + << false << false + << false << QByteArray() + << false << EwsItem(); + + QTest::newRow("unknown type") + << xmlHead + << true + << EwsAttachment::UnknownAttachment + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << 0l + << false << QDateTime() + << false << false + << false << false + << false << QByteArray() + << false << EwsItem(); + + QTest::newRow("empty file attachment") + << xmlHead + QStringLiteral("") + << true + << EwsAttachment::FileAttachment + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << 0l + << false << QDateTime() + << false << false + << false << false + << false << QByteArray() + << false << EwsItem(); + + QTest::newRow("empty item attachment") + << xmlHead + QStringLiteral("") + << true + << EwsAttachment::ItemAttachment + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << 0l + << false << QDateTime() + << false << false + << false << false + << false << QByteArray() + << false << EwsItem(); + + QTest::newRow("empty reference attachment") + << xmlHead + QStringLiteral("") + << true + << EwsAttachment::ReferenceAttachment + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << 0l + << false << QDateTime() + << false << false + << false << false + << false << QByteArray() + << false << EwsItem(); + + QTest::newRow("non-empty reference attachment") + << xmlHead + QStringLiteral("") + << true + << EwsAttachment::ReferenceAttachment + << true << QStringLiteral("5IaIqJVsJzamf2105wg4wQ==") + << false << QString() + << false << QString() + << false << QString() + << false << QString() + << false << 0l + << false << QDateTime() + << false << false + << false << false + << false << QByteArray() + << false << EwsItem(); + + EwsItem item1; + item1.setType(EwsItemTypeItem); + item1.setField(EwsItemFieldItemId, QVariant::fromValue(EwsId(QStringLiteral("VGhpcyBpcyBhIHRlc3Q="), QStringLiteral("muKls0n8pUM=")))); + + QTest::newRow("non-empty item attachment") + << xmlHead + QStringLiteral("" + "" + "application/x-test" + "file:///foo/bar" + "2017-01-05T14:00:43" + "" + "") + << true + << EwsAttachment::ItemAttachment + << true << QStringLiteral("5IaIqJVsJzamf2105wg4wQ==") + << false << QStringLiteral("Test attachment") + << true << QStringLiteral("application/x-test") + << false << QStringLiteral("FE938BD618330B9DA0C965A6077BB3FF20415531@1") + << true << QStringLiteral("file:///foo/bar") + << false << 123l + << true << QDateTime::fromTime_t(1483621243) + << false << true + << true << true + << true << QByteArray("This is another test") + << true << item1; + + QTest::newRow("non-empty file attachment") + << xmlHead + QStringLiteral("" + "Test attachment" + "FE938BD618330B9DA0C965A6077BB3FF20415531@1" + "123" + "true" + "true" + "VGhpcyBpcyBhbm90aGVyIHRlc3Q=" + "") + << true + << EwsAttachment::FileAttachment + << false << QStringLiteral("5IaIqJVsJzamf2105wg4wQ==") + << true << QStringLiteral("Test attachment") + << false << QStringLiteral("application/x-test") + << true << QStringLiteral("FE938BD618330B9DA0C965A6077BB3FF20415531@1") + << false << QStringLiteral("file:///foo/bar") + << true << 123l + << false << QDateTime::fromTime_t(1483621243) + << true << true + << true << true + << true << QByteArray("This is another test") + << true << item1; + +} + +QTEST_MAIN(UtEwsAttachment) + +#include "ewsattachment_ut.moc" diff --git a/resources/ews/test/unittests/ewsdeleteitemrequest_ut.cpp b/resources/ews/test/unittests/ewsdeleteitemrequest_ut.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/test/unittests/ewsdeleteitemrequest_ut.cpp @@ -0,0 +1,305 @@ +/* + 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 +#include + +#include "fakehttppost.h" + +#include "ewsdeleteitemrequest.h" + +class UtEwsDeleteItemRequest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void singleItem(); + void twoItems(); + void twoItemsOneFailed(); + void twoItemsSecondFailed(); +private: + void verifier(FakeTransferJob* job, const QByteArray &req, const QByteArray &expReq, + const QByteArray &resp); + + EwsClient mClient; +}; + +void UtEwsDeleteItemRequest::singleItem() +{ + static const QByteArray request = "" + "" + "" + "" + "" + "\n"; + static const QByteArray response = "" + "" + "" + "" + "" + "" + "" + "" + "" + "NoError" + "" + "" + "" + "" + ""; + + FakeTransferJob::addVerifier(this, [this](FakeTransferJob* job, const QByteArray &req){ + verifier(job, req, request, response); + }); + QScopedPointer req(new EwsDeleteItemRequest(mClient, this)); + EwsId::List ids; + ids << EwsId(QStringLiteral("+IRgnMJ8x+J6MQAZ"), QStringLiteral("1iQt/At9")); + req->setItemIds(ids); + req->exec(); + + QCOMPARE(req->error(), 0); + QCOMPARE(req->responses().size(), 1); + EwsDeleteItemRequest::Response resp = req->responses().first(); + QCOMPARE(resp.responseClass(), EwsResponseSuccess); +} + +void UtEwsDeleteItemRequest::twoItems() +{ + static const QByteArray request = "" + "" + "" + "" + "" + "" + "" + "" + "\n"; + static const QByteArray response = "" + "" + "" + "" + "" + "" + "" + "" + "" + "NoError" + "" + "" + "NoError" + "" + "" + "" + "" + ""; + + FakeTransferJob::addVerifier(this, [this](FakeTransferJob* job, const QByteArray &req){ + verifier(job, req, request, response); + }); + QScopedPointer req(new EwsDeleteItemRequest(mClient, this)); + static const EwsId::List ids = { + EwsId(QStringLiteral("9LB1MiL3cOYUjmYy"), QStringLiteral("TBjl3rnU")), + EwsId(QStringLiteral("rZ0sc7Gfn9+XHVgv"), QStringLiteral("pHTEe9nY")) + }; + req->setItemIds(ids); + req->exec(); + + QCOMPARE(req->error(), 0); + QCOMPARE(req->responses().size(), 2); + Q_FOREACH(const EwsDeleteItemRequest::Response &resp, req->responses()) { + QCOMPARE(resp.responseClass(), EwsResponseSuccess); + } +} + +void UtEwsDeleteItemRequest::twoItemsOneFailed() +{ + static const QByteArray request = "" + "" + "" + "" + "" + "" + "" + "" + "\n"; + static const QByteArray response = "" + "" + "" + "" + "" + "" + "" + "" + "" + "The specified object was not found in the store." + "ErrorItemNotFound" + "0" + "" + "" + "NoError" + "" + "" + "" + "" + ""; + + FakeTransferJob::addVerifier(this, [this](FakeTransferJob* job, const QByteArray &req){ + verifier(job, req, request, response); + }); + QScopedPointer req(new EwsDeleteItemRequest(mClient, this)); + static const EwsId::List ids = { + EwsId(QStringLiteral("9LB1MiL3cOYUjmYy"), QStringLiteral("TBjl3rnU")), + EwsId(QStringLiteral("rZ0sc7Gfn9+XHVgv"), QStringLiteral("pHTEe9nY")) + }; + req->setItemIds(ids); + req->exec(); + + QCOMPARE(req->error(), 0); + QCOMPARE(req->responses().size(), 2); + static const QList respClasses = { + EwsResponseError, + EwsResponseSuccess + }; + QList::const_iterator respClassesIt = respClasses.begin(); + unsigned i = 0; + Q_FOREACH(const EwsDeleteItemRequest::Response &resp, req->responses()) { + qDebug() << "Verifying response" << i++; + QCOMPARE(resp.responseClass(), *respClassesIt); + respClassesIt++; + } +} + +void UtEwsDeleteItemRequest::twoItemsSecondFailed() +{ + static const QByteArray request = "" + "" + "" + "" + "" + "" + "" + "" + "\n"; + static const QByteArray response = "" + "" + "" + "" + "" + "" + "" + "" + "" + "The specified object was not found in the store." + "ErrorItemNotFound" + "0" + "" + "" + "NoError" + "" + "" + "" + "" + ""; + + FakeTransferJob::addVerifier(this, [this](FakeTransferJob* job, const QByteArray &req){ + verifier(job, req, request, response); + }); + QScopedPointer req(new EwsDeleteItemRequest(mClient, this)); + static const EwsId::List ids = { + EwsId(QStringLiteral("9LB1MiL3cOYUjmYy"), QStringLiteral("TBjl3rnU")), + EwsId(QStringLiteral("rZ0sc7Gfn9+XHVgv"), QStringLiteral("pHTEe9nY")) + }; + req->setItemIds(ids); + req->exec(); + + QCOMPARE(req->error(), 0); + QCOMPARE(req->responses().size(), 2); + static const QList respClasses = { + EwsResponseError, + EwsResponseSuccess + }; + QList::const_iterator respClassesIt = respClasses.begin(); + unsigned i = 0; + Q_FOREACH(const EwsDeleteItemRequest::Response &resp, req->responses()) { + qDebug() << "Verifying response" << i++; + QCOMPARE(resp.responseClass(), *respClassesIt); + respClassesIt++; + } +} + +void UtEwsDeleteItemRequest::verifier(FakeTransferJob* job, const QByteArray &req, + const QByteArray &expReq, const QByteArray &response) +{ + bool fail = true; + auto f = finally([&fail,&job]{ + if (fail) { + job->postResponse(""); + } + }); + QCOMPARE(req, expReq); + fail = false; + job->postResponse(response); +} + +QTEST_MAIN(UtEwsDeleteItemRequest) + +#include "ewsdeleteitemrequest_ut.moc" diff --git a/resources/ews/test/unittests/ewsgetitemrequest_ut.cpp b/resources/ews/test/unittests/ewsgetitemrequest_ut.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/test/unittests/ewsgetitemrequest_ut.cpp @@ -0,0 +1,148 @@ +/* + 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 +#include + +#include "fakehttppost.h" + +#include "ewsgetitemrequest.h" + +class UtEwsGetItemRequest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void twoFailures(); +private: + void verifier(FakeTransferJob* job, const QByteArray &req, const QByteArray &expReq, + const QByteArray &resp); + + EwsClient mClient; +}; + +void UtEwsGetItemRequest::twoFailures() +{ + static const QByteArray request = "" + "" + "" + "" + "" + "" + "IdOnly" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "\n"; + static const QByteArray response = "" + "" + "" + "" + "" + "" + "" + "" + "NoError" + "NoError" + "NoError" + "NoError" + "NoError" + "NoError" + "NoError" + "The specified object was not found in the store.ErrorItemNotFound0" + "The specified object was not found in the store.ErrorItemNotFound0" + ""; + + FakeTransferJob::addVerifier(this, [this](FakeTransferJob* job, const QByteArray &req){ + verifier(job, req, request, response); + }); + QScopedPointer req(new EwsGetItemRequest(mClient, this)); + EwsId::List ids; + ids << EwsId(QStringLiteral("DdBTBAvLHI8OyQ3K"), QStringLiteral("6yDDqXl+")); + ids << EwsId(QStringLiteral("CgIdfZGT3QJrWZHi"), QStringLiteral("wPjRsOpg")); + ids << EwsId(QStringLiteral("Enxw15n4imIERH4w"), QStringLiteral("82pEQQIj")); + ids << EwsId(QStringLiteral("yV1OhxPOinZ7mxpK"), QStringLiteral("B22tdkME")); + ids << EwsId(QStringLiteral("j1ptydBqXKJLuCiB"), QStringLiteral("z0u+e6/Z")); + ids << EwsId(QStringLiteral("ogM0ejAHml/og1tZ"), QStringLiteral("f2t/ou/g")); + ids << EwsId(QStringLiteral("ZqDVkG1gIrUkJDGB"), QStringLiteral("0LOh2uE+")); + ids << EwsId(QStringLiteral("SFYgXaYm1DK+0TCs"), QStringLiteral("Zbkp+aB4")); + ids << EwsId(QStringLiteral("UrNr/v4HynI062u/"), QStringLiteral("WMjq6rUe")); + req->setItemIds(ids); + + req->setItemShape(EwsItemShape(EwsShapeIdOnly)); + + req->exec(); + + QCOMPARE(req->error(), 0); + QCOMPARE(req->responses().size(), ids.size()); + + static const QList respClasses = { + EwsResponseSuccess, + EwsResponseSuccess, + EwsResponseSuccess, + EwsResponseSuccess, + EwsResponseSuccess, + EwsResponseSuccess, + EwsResponseSuccess, + EwsResponseError, + EwsResponseError + }; + QList::const_iterator respClassesIt = respClasses.begin(); + EwsId::List::const_iterator idsIt = ids.cbegin(); + unsigned i = 0; + Q_FOREACH(const EwsGetItemRequest::Response &resp, req->responses()) { + qDebug() << "Verifying response" << i++; + QCOMPARE(resp.responseClass(), *respClassesIt); + if (resp.isSuccess()) { + EwsId id = resp.item()[EwsItemFieldItemId].value(); + QCOMPARE(id, *idsIt); + } + idsIt++; + respClassesIt++; + } +} + +void UtEwsGetItemRequest::verifier(FakeTransferJob* job, const QByteArray &req, + const QByteArray &expReq, const QByteArray &response) +{ + bool fail = true; + auto f = finally([&fail,&job]{ + if (fail) { + job->postResponse(""); + } + }); + QCOMPARE(req, expReq); + fail = false; + job->postResponse(response); +} + +QTEST_MAIN(UtEwsGetItemRequest) + +#include "ewsgetitemrequest_ut.moc" diff --git a/resources/ews/test/unittests/ewsmoveitemrequest_ut.cpp b/resources/ews/test/unittests/ewsmoveitemrequest_ut.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/test/unittests/ewsmoveitemrequest_ut.cpp @@ -0,0 +1,371 @@ +/* + 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 +#include + +#include "fakehttppost.h" + +#include "ewsmoveitemrequest.h" + +class UtEwsMoveItemRequest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void singleItem(); + void twoItems(); + void twoItemsOneFailed(); + void twoItemsSecondFailed(); +private: + void verifier(FakeTransferJob* job, const QByteArray &req, const QByteArray &expReq, + const QByteArray &resp); + + EwsClient mClient; +}; + +void UtEwsMoveItemRequest::singleItem() +{ + static const QByteArray request = "" + "" + "" + "" + "" + "" + "" + "\n"; + static const QByteArray response = "" + "" + "" + "" + "" + "" + "" + "" + "" + "NoError" + "" + "" + "" + "" + "" + "" + "" + "" + ""; + + FakeTransferJob::addVerifier(this, [this](FakeTransferJob* job, const QByteArray &req){ + verifier(job, req, request, response); + }); + QScopedPointer req(new EwsMoveItemRequest(mClient, this)); + EwsId::List ids; + ids << EwsId(QStringLiteral("Xnn2DwwaXQUhbn7U"), QStringLiteral("rqs77HkG")); + req->setItemIds(ids); + req->setDestinationFolderId(EwsId(QStringLiteral("R70cDGNT1SqOk2pn"), QStringLiteral("1DjfJ3dT"))); + req->exec(); + + QCOMPARE(req->error(), 0); + QCOMPARE(req->responses().size(), 1); + EwsMoveItemRequest::Response resp = req->responses().first(); + QCOMPARE(resp.responseClass(), EwsResponseSuccess); + QCOMPARE(resp.itemId(), EwsId(QStringLiteral("Xnn2DwwaXQUhbn7U"), QStringLiteral("JoFvRwDP"))); +} + +void UtEwsMoveItemRequest::twoItems() +{ + static const QByteArray request = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "\n"; + static const QByteArray response = "" + "" + "" + "" + "" + "" + "" + "" + "" + "NoError" + "" + "" + "" + "" + "" + "" + "NoError" + "" + "" + "" + "" + "" + "" + "" + "" + ""; + + FakeTransferJob::addVerifier(this, [this](FakeTransferJob* job, const QByteArray &req){ + verifier(job, req, request, response); + }); + QScopedPointer req(new EwsMoveItemRequest(mClient, this)); + static const EwsId::List ids = { + EwsId(QStringLiteral("Xnn2DwwaXQUhbn7U"), QStringLiteral("rqs77HkG")), + EwsId(QStringLiteral("ntTNOncESwiyAXog"), QStringLiteral("EDHu5rwK")) + }; + req->setItemIds(ids); + req->setDestinationFolderId(EwsId(QStringLiteral("R70cDGNT1SqOk2pn"), QStringLiteral("1DjfJ3dT"))); + req->exec(); + + QCOMPARE(req->error(), 0); + QCOMPARE(req->responses().size(), 2); + static const EwsId::List newIds = { + EwsId(QStringLiteral("Xnn2DwwaXQUhbn7U"), QStringLiteral("JoFvRwDP")), + EwsId(QStringLiteral("ntTNOncESwiyAXog"), QStringLiteral("4qbAwd3y")) + }; + EwsId::List::const_iterator newIdsIt = newIds.begin(); + Q_FOREACH(const EwsMoveItemRequest::Response &resp, req->responses()) { + QCOMPARE(resp.responseClass(), EwsResponseSuccess); + QCOMPARE(resp.itemId(), *newIdsIt); + newIdsIt++; + } +} + +void UtEwsMoveItemRequest::twoItemsOneFailed() +{ + static const QByteArray request = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "\n"; + static const QByteArray response = "" + "" + "" + "" + "" + "" + "" + "" + "" + "NoError" + "" + "" + "" + "" + "" + "" + "The specified object was not found in the store." + "ErrorItemNotFound" + "0" + "" + "" + "" + "" + "" + ""; + + FakeTransferJob::addVerifier(this, [this](FakeTransferJob* job, const QByteArray &req){ + verifier(job, req, request, response); + }); + QScopedPointer req(new EwsMoveItemRequest(mClient, this)); + static const EwsId::List ids = { + EwsId(QStringLiteral("Xnn2DwwaXQUhbn7U"), QStringLiteral("rqs77HkG")), + EwsId(QStringLiteral("ntTNOncESwiyAXog"), QStringLiteral("EDHu5rwK")) + }; + req->setItemIds(ids); + req->setDestinationFolderId(EwsId(QStringLiteral("R70cDGNT1SqOk2pn"), QStringLiteral("1DjfJ3dT"))); + req->exec(); + + QCOMPARE(req->error(), 0); + QCOMPARE(req->responses().size(), 2); + static const QList respClasses = { + EwsResponseSuccess, + EwsResponseError + }; + static const EwsId::List newIds = { + EwsId(QStringLiteral("Xnn2DwwaXQUhbn7U"), QStringLiteral("JoFvRwDP")), + EwsId(QStringLiteral("ntTNOncESwiyAXog"), QStringLiteral("EDHu5rwK")) + }; + EwsId::List::const_iterator newIdsIt = newIds.begin(); + QList::const_iterator respClassesIt = respClasses.begin(); + unsigned i = 0; + Q_FOREACH(const EwsMoveItemRequest::Response &resp, req->responses()) { + qDebug() << "Verifying response" << i++; + QCOMPARE(resp.responseClass(), *respClassesIt); + if (resp.isSuccess()) { + QCOMPARE(resp.itemId(), *newIdsIt); + } + else { + QCOMPARE(resp.itemId(), EwsId()); + } + newIdsIt++; + respClassesIt++; + } +} + +void UtEwsMoveItemRequest::twoItemsSecondFailed() +{ + static const QByteArray request = "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "\n"; + static const QByteArray response = "" + "" + "" + "" + "" + "" + "" + "" + "" + "The specified object was not found in the store." + "ErrorItemNotFound" + "0" + "" + "" + "" + "NoError" + "" + "" + "" + "" + "" + "" + "" + "" + ""; + + FakeTransferJob::addVerifier(this, [this](FakeTransferJob* job, const QByteArray &req){ + verifier(job, req, request, response); + }); + QScopedPointer req(new EwsMoveItemRequest(mClient, this)); + static const EwsId::List ids = { + EwsId(QStringLiteral("Xnn2DwwaXQUhbn7U"), QStringLiteral("rqs77HkG")), + EwsId(QStringLiteral("ntTNOncESwiyAXog"), QStringLiteral("EDHu5rwK")) + }; + req->setItemIds(ids); + req->setDestinationFolderId(EwsId(QStringLiteral("R70cDGNT1SqOk2pn"), QStringLiteral("1DjfJ3dT"))); + req->exec(); + + QCOMPARE(req->error(), 0); + QCOMPARE(req->responses().size(), 2); + static const QList respClasses = { + EwsResponseError, + EwsResponseSuccess + }; + static const EwsId::List newIds = { + EwsId(QStringLiteral("Xnn2DwwaXQUhbn7U"), QStringLiteral("JoFvRwDP")), + EwsId(QStringLiteral("ntTNOncESwiyAXog"), QStringLiteral("4qbAwd3y")) + }; + EwsId::List::const_iterator newIdsIt = newIds.begin(); + QList::const_iterator respClassesIt = respClasses.begin(); + unsigned i = 0; + Q_FOREACH(const EwsMoveItemRequest::Response &resp, req->responses()) { + qDebug() << "Verifying response" << i++; + QCOMPARE(resp.responseClass(), *respClassesIt); + if (resp.isSuccess()) { + QCOMPARE(resp.itemId(), *newIdsIt); + } + else { + QCOMPARE(resp.itemId(), EwsId()); + } + newIdsIt++; + respClassesIt++; + } +} + +void UtEwsMoveItemRequest::verifier(FakeTransferJob* job, const QByteArray &req, + const QByteArray &expReq, const QByteArray &response) +{ + bool fail = true; + auto f = finally([&fail,&job]{ + if (fail) { + job->postResponse(""); + } + }); + QCOMPARE(req, expReq); + fail = false; + job->postResponse(response); +} + +QTEST_MAIN(UtEwsMoveItemRequest) + +#include "ewsmoveitemrequest_ut.moc" diff --git a/resources/ews/test/unittests/ewsunsubscriberequest_ut.cpp b/resources/ews/test/unittests/ewsunsubscriberequest_ut.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/test/unittests/ewsunsubscriberequest_ut.cpp @@ -0,0 +1,97 @@ +/* + 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 +#include + +#include "fakehttppost.h" + +#include "ewsunsubscriberequest.h" + +class UtEwsUnsibscribeRequest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void simple(); +private: + void verifier(FakeTransferJob* job, const QByteArray &req, const QByteArray &expReq, + const QByteArray &resp); + + EwsClient mClient; +}; + +void UtEwsUnsibscribeRequest::simple() +{ + static const QByteArray request = "" + "" + "" + "" + "" + "dwzVKTlwXxBZtQRMucP5Mg==" + "\n"; + static const QByteArray response = "" + "" + "" + "" + "" + "" + "" + "" + "NoError" + "" + "" + "" + ""; + + FakeTransferJob::addVerifier(this, [this](FakeTransferJob* job, const QByteArray &req){ + verifier(job, req, request, response); + }); + QScopedPointer req(new EwsUnsubscribeRequest(mClient, this)); + req->setSubscriptionId(QStringLiteral("dwzVKTlwXxBZtQRMucP5Mg==")); + + req->exec(); + + QCOMPARE(req->error(), 0); +} + +void UtEwsUnsibscribeRequest::verifier(FakeTransferJob* job, const QByteArray &req, + const QByteArray &expReq, const QByteArray &response) +{ + bool fail = true; + auto f = finally([&fail,&job]{ + if (fail) { + job->postResponse(""); + } + }); + QCOMPARE(req, expReq); + fail = false; + job->postResponse(response); +} + +QTEST_MAIN(UtEwsUnsibscribeRequest) + +#include "ewsunsubscriberequest_ut.moc" diff --git a/resources/ews/test/unittests/fakehttppost.h b/resources/ews/test/unittests/fakehttppost.h new file mode 100644 --- /dev/null +++ b/resources/ews/test/unittests/fakehttppost.h @@ -0,0 +1,44 @@ +/* + 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 FAKEHTTPPOST_H +#define FAKEHTTPPOST_H + +#include + +#include + +#include "faketransferjob.h" + +namespace KIO +{ + +TransferJob *http_post(const QUrl &url, const QByteArray &postData, JobFlags flags) +{ + Q_UNUSED(url); + Q_UNUSED(flags); + + FakeTransferJob::Verifier vfy = FakeTransferJob::getVerifier(); + FakeTransferJob *job = new FakeTransferJob(postData, vfy.fn, vfy.object); + return reinterpret_cast(job); +} + +} + +#endif diff --git a/resources/ews/test/unittests/faketransferjob.h b/resources/ews/test/unittests/faketransferjob.h new file mode 100644 --- /dev/null +++ b/resources/ews/test/unittests/faketransferjob.h @@ -0,0 +1,83 @@ +/* + 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 + +#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(); + + static void addVerifier(QObject *obj, 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/ews/test/unittests/faketransferjob.cpp b/resources/ews/test/unittests/faketransferjob.cpp new file mode 100644 --- /dev/null +++ b/resources/ews/test/unittests/faketransferjob.cpp @@ -0,0 +1,73 @@ +/* + 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 + +#include + +QQueue FakeTransferJob::mVerifierQueue; + +FakeTransferJob::FakeTransferJob(const QByteArray &postData, VerifierFn fn, QObject *parent) + : KIO::SpecialJob(QUrl(QStringLiteral("file:///tmp/")), QByteArray()), mPostData(postData), + mVerifier(fn) +{ + 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(const 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) +{ + Verifier vfy = {obj, fn}; + mVerifierQueue.enqueue(vfy); +} + +FakeTransferJob::Verifier FakeTransferJob::getVerifier() +{ + return mVerifierQueue.dequeue(); +}