diff --git a/CMakeLists.txt b/CMakeLists.txt index 279be17..69329a0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,94 +1,93 @@ cmake_minimum_required(VERSION 2.8.12) project(Akonadi-Calendar) # ECM setup set(KF5_VERSION "5.28.0") find_package(ECM ${KF5_VERSION} CONFIG REQUIRED) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) include(GenerateExportHeader) include(ECMGenerateHeaders) include(ECMGeneratePriFile) include(ECMPackageConfigHelpers) include(ECMSetupVersion) include(FeatureSummary) include(KDEInstallDirs) include(KDECMakeSettings) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(ECMQtDeclareLoggingCategory) set(PIM_VERSION "5.4.3") set(AKONADICALENDAR_LIB_VERSION ${PIM_VERSION}) set(CALENDARCORE_LIB_VERSION "5.4.3") set(AKONADICONTACT_LIB_VERSION "5.4.3") set(AKONADI_LIB_VERSION "5.4.3") set(MAILTRANSPORT_LIB_VERSION "5.4.3") set(KCONTACTS_LIB_VERSION "5.4.3") set(CALENDARUTILS_LIB_VERSION "5.4.3") set(IDENTITYMANAGEMENT_LIB_VERSION "5.4.3") ecm_setup_version(${AKONADICALENDAR_LIB_VERSION} VARIABLE_PREFIX AKONADICALENDAR VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/akonadi-calendar_version.h" PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KF5AkonadiCalendarConfigVersion.cmake" SOVERSION 5 ) configure_file(akonadi-calendar-version.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/src/akonadi-calendar-version.h @ONLY) +if(BUILD_TESTING) + set(AKONADI_CALENDAR_TESTS_EXPORT AKONADI_CALENDAR_EXPORT) +endif() +configure_file(akonadi-calendar_tests_export.h.in "${CMAKE_CURRENT_BINARY_DIR}/src/akonadi-calendar_tests_export.h" @ONLY) + ########### Find packages ########### find_package(KF5KIO ${KF5_VERSION} CONFIG REQUIRED) find_package(KF5Wallet ${KF5_VERSION} CONFIG REQUIRED) find_package(KF5Codecs ${KF5_VERSION} CONFIG REQUIRED) find_package(KF5MailTransport ${MAILTRANSPORT_LIB_VERSION} CONFIG REQUIRED) find_package(KF5Contacts ${KCONTACTS_LIB_VERSION} CONFIG REQUIRED) find_package(KF5IdentityManagement ${IDENTITYMANAGEMENT_LIB_VERSION} CONFIG REQUIRED) find_package(KF5CalendarCore ${CALENDARCORE_LIB_VERSION} CONFIG REQUIRED) find_package(KF5CalendarUtils ${CALENDARUTILS_LIB_VERSION} CONFIG REQUIRED) find_package(KF5Akonadi ${AKONADI_LIB_VERSION} CONFIG REQUIRED) find_package(KF5AkonadiContact ${AKONADICONTACT_LIB_VERSION} CONFIG REQUIRED) ########### Targets ########### add_definitions(-DTRANSLATION_DOMAIN=\"libakonadi-calendar5\") add_definitions("-DQT_NO_CAST_FROM_ASCII -DQT_NO_CAST_TO_ASCII") -# TODO: switch to more standard approach for export-class-if-building-autotests once the tests pass -set( PLEASE_TEST_INVITATIONS FALSE) -if ( PLEASE_TEST_INVITATIONS ) - add_definitions( -DPLEASE_TEST_INVITATIONS ) -endif() - add_subdirectory(src) if (BUILD_TESTING) add_subdirectory(autotests) endif () ########### CMake Config Files ########### set(CMAKECONFIG_INSTALL_DIR "${KDE_INSTALL_CMAKEPACKAGEDIR}/KF5AkonadiCalendar") ecm_configure_package_config_file( "${CMAKE_CURRENT_SOURCE_DIR}/KF5AkonadiCalendarConfig.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/KF5AkonadiCalendarConfig.cmake" INSTALL_DESTINATION ${CMAKECONFIG_INSTALL_DIR} ) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/KF5AkonadiCalendarConfig.cmake" "${CMAKE_CURRENT_BINARY_DIR}/KF5AkonadiCalendarConfigVersion.cmake" DESTINATION "${CMAKECONFIG_INSTALL_DIR}" COMPONENT Devel ) install(EXPORT KF5AkonadiCalendarTargets DESTINATION "${CMAKECONFIG_INSTALL_DIR}" FILE KF5AkonadiCalendarTargets.cmake NAMESPACE KF5::) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/akonadi-calendar_version.h DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5} COMPONENT Devel ) feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/akonadi-calendar_tests_export.h.in b/akonadi-calendar_tests_export.h.in new file mode 100644 index 0000000..7540dbb --- /dev/null +++ b/akonadi-calendar_tests_export.h.in @@ -0,0 +1,2 @@ +#include "akonadi-calendar_export.h" +#define AKONADI_CALENDAR_TESTS_EXPORT @AKONADI_CALENDAR_TESTS_EXPORT@ diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index e8e018a..1127167 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -1,32 +1,28 @@ include(ECMMarkAsTest) set(QT_REQUIRED_VERSION "5.6.0") find_package(Qt5Test ${QT_REQUIRED_VERSION} CONFIG REQUIRED) set( KDEPIMLIBS_RUN_ISOLATED_TESTS TRUE ) set( PREVIOUS_EXEC_OUTPUT_PATH ../../tests ) set( EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR} ) -add_definitions(-DITIP_DATA_DIR="\\"${CMAKE_CURRENT_SOURCE_DIR}/itip_data\\"" ) - set(common_sources unittestbase.cpp helper.cpp ${Akonadi-Calendar_BINARY_DIR}/src/akonadicalendar_debug.cpp ${Akonadi-Calendar_SOURCE_DIR}/src/utils_p.cpp) set(common_libs "KF5::AkonadiCalendar;KF5::CalendarCore;KF5::Mime;KF5::IdentityManagement;KF5::AkonadiWidgets") # the tests need the ical resource, which we might not have at this point (e.g. on the CI) find_program(AKONADI_ICAL_RESOURCE NAMES akonadi_ical_resource) if (AKONADI_ICAL_RESOURCE) add_akonadi_isolated_test_advanced( historytest.cpp "${common_sources}" "${common_libs}") add_akonadi_isolated_test_advanced( incidencechangertest.cpp "" "KF5::AkonadiCalendar" ) add_akonadi_isolated_test_advanced( calendarbasetest.cpp "" "KF5::AkonadiCalendar" ) add_akonadi_isolated_test_advanced( fetchjobcalendartest.cpp "" "KF5::AkonadiCalendar" ) add_akonadi_isolated_test_advanced( etmcalendartest.cpp "" "KF5::AkonadiCalendar" ) +add_akonadi_isolated_test_advanced( itiphandlertest.cpp "${common_sources}" "${common_libs};KF5::MailTransport") +add_akonadi_isolated_test_advanced( mailclienttest.cpp "" "KF5::AkonadiCalendar;KF5::Mime;KF5::MailTransport;KF5::IdentityManagement") -if ( PLEASE_TEST_INVITATIONS ) - add_akonadi_isolated_test_advanced( itiphandlertest.cpp "${common_sources}" "${common_libs};KF5::MailTransport") - add_akonadi_isolated_test_advanced( mailclienttest.cpp "" "KF5::AkonadiCalendar;KF5::Mime;KF5::MailTransport;KF5::IdentityManagement") -endif() endif() diff --git a/autotests/helper.cpp b/autotests/helper.cpp index d6a57aa..95bac78 100644 --- a/autotests/helper.cpp +++ b/autotests/helper.cpp @@ -1,40 +1,41 @@ #include "helper.h" #include #include #include #include using namespace Akonadi; bool Helper::confirmExists(const Akonadi::Item &item) { ItemFetchJob *job = new ItemFetchJob(item); return job->exec() != 0; } bool Helper::confirmDoesntExist(const Akonadi::Item &item) { ItemFetchJob *job = new ItemFetchJob(item); return job->exec() == 0; } Akonadi::Collection Helper::fetchCollection() { CollectionFetchJob *job = new CollectionFetchJob(Collection::root(), CollectionFetchJob::Recursive); // Get list of collections job->fetchScope().setContentMimeTypes(QStringList() << QStringLiteral("application/x-vnd.akonadi.calendar.event")); const bool ret = job->exec(); Q_ASSERT(ret); Q_UNUSED(ret); // Find our collection - Collection::List collections = job->collections(); + const Collection::List collections = job->collections(); + Q_ASSERT(!collections.isEmpty()); Collection collection = collections.first(); Q_ASSERT(collection.isValid()); return collection; } diff --git a/autotests/itiphandlertest.cpp b/autotests/itiphandlertest.cpp index f67afc9..b7cc244 100644 --- a/autotests/itiphandlertest.cpp +++ b/autotests/itiphandlertest.cpp @@ -1,738 +1,740 @@ /* Copyright (c) 2013 Sérgio Martins 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 "itiphandlertest.h" #include "helper.h" #include "mailclient_p.h" #include "fetchjobcalendar.h" #include "utils_p.h" #include #include #include #include #include #include #include #include #include #include using namespace Akonadi; using namespace KCalCore; Q_DECLARE_METATYPE(Akonadi::IncidenceChanger::InvitationPolicy) Q_DECLARE_METATYPE(QList) Q_DECLARE_METATYPE(Akonadi::ITIPHandler::Result) Q_DECLARE_METATYPE(KCalCore::Attendee::PartStat) Q_DECLARE_METATYPE(QList) static const char *s_ourEmail = "unittests@dev.nul"; // change also in kdepimlibs/akonadi/calendar/tests/unittestenv/kdehome/share/config static const char *s_outEmail2 = "identity2@kde.org"; class FakeMessageQueueJob : public MailTransport::MessageQueueJob { public: explicit FakeMessageQueueJob(QObject *parent = Q_NULLPTR) : MailTransport::MessageQueueJob(parent) { } virtual void start() { UnitTestResult unitTestResult; unitTestResult.message = message(); unitTestResult.from = addressAttribute().from(); unitTestResult.to = addressAttribute().to(); unitTestResult.cc = addressAttribute().cc(); unitTestResult.bcc = addressAttribute().bcc(); unitTestResult.transportId = transportAttribute().transportId(); FakeMessageQueueJob::sUnitTestResults << unitTestResult; setError(Akonadi::MailClient::ResultSuccess); setErrorText(QString()); emitResult(); } static UnitTestResult::List sUnitTestResults; }; UnitTestResult::List FakeMessageQueueJob::sUnitTestResults; class FakeITIPHandlerComponentFactory : public ITIPHandlerComponentFactory { public: FakeITIPHandlerComponentFactory(QObject *parent = Q_NULLPTR) : ITIPHandlerComponentFactory(parent) { } virtual MailTransport::MessageQueueJob *createMessageQueueJob(const KCalCore::IncidenceBase::Ptr &incidence, const KIdentityManagement::Identity &identity, QObject *parent = 0) { Q_UNUSED(incidence); Q_UNUSED(identity); return new FakeMessageQueueJob(parent); } }; void ITIPHandlerTest::initTestCase() { AkonadiTest::checkTestIsIsolated(); + extern AKONADI_CALENDAR_TESTS_EXPORT bool akonadi_calendar_running_unittests; + akonadi_calendar_running_unittests = true; m_pendingItipMessageSignal = 0; m_pendingIncidenceChangerSignal = 0; m_itipHandler = 0; m_cancelExpected = false; m_changer = new IncidenceChanger(new FakeITIPHandlerComponentFactory(this), this); m_changer->setHistoryEnabled(false); m_changer->setGroupwareCommunication(true); m_changer->setInvitationPolicy(IncidenceChanger::InvitationPolicySend); // don't show dialogs connect(m_changer, SIGNAL(createFinished(int,Akonadi::Item,Akonadi::IncidenceChanger::ResultCode,QString)), SLOT(onCreateFinished(int,Akonadi::Item,Akonadi::IncidenceChanger::ResultCode,QString))); connect(m_changer, SIGNAL(deleteFinished(int,QVector,Akonadi::IncidenceChanger::ResultCode,QString)), SLOT(onDeleteFinished(int,QVector,Akonadi::IncidenceChanger::ResultCode,QString))); connect(m_changer, SIGNAL(modifyFinished(int,Akonadi::Item,Akonadi::IncidenceChanger::ResultCode,QString)), SLOT(onModifyFinished(int,Akonadi::Item,Akonadi::IncidenceChanger::ResultCode,QString))); } void ITIPHandlerTest::testProcessITIPMessage_data() { QTest::addColumn("data_filename"); QTest::addColumn("action"); QTest::addColumn("receiver"); QTest::addColumn("incidenceUid"); // uid of incidence in invitation QTest::addColumn("expectedResult"); QTest::addColumn("expectedNumIncidences"); QTest::addColumn("expectedPartStat"); QString data_filename; QString action = QStringLiteral("accepted"); QString incidenceUid = QStringLiteral("uosj936i6arrtl9c2i5r2mfuvg"); QString receiver = QLatin1String(s_ourEmail); Akonadi::ITIPHandler::Result expectedResult; int expectedNumIncidences = 0; KCalCore::Attendee::PartStat expectedPartStat; //---------------------------------------------------------------------------------------------- // Someone invited us to an event, and we accept expectedResult = ITIPHandler::ResultSuccess; data_filename = QStringLiteral("invited_us"); expectedNumIncidences = 1; expectedPartStat = KCalCore::Attendee::Accepted; action = QStringLiteral("accepted"); QTest::newRow("invited us1") << data_filename << action << receiver << incidenceUid << expectedResult << expectedNumIncidences << expectedPartStat; //---------------------------------------------------------------------------------------------- // Someone invited us to an event, and we accept conditionally expectedResult = ITIPHandler::ResultSuccess; data_filename = QStringLiteral("invited_us"); expectedNumIncidences = 1; expectedPartStat = KCalCore::Attendee::Tentative; action = QStringLiteral("tentative"); QTest::newRow("invited us2") << data_filename << action << receiver << incidenceUid << expectedResult << expectedNumIncidences << expectedPartStat; //---------------------------------------------------------------------------------------------- // Someone invited us to an event, we delegate it expectedResult = ITIPHandler::ResultSuccess; data_filename = QStringLiteral("invited_us"); // The e-mail to the delegate is sent by kmail's text_calendar.cpp expectedNumIncidences = 1; expectedPartStat = KCalCore::Attendee::Delegated; action = QStringLiteral("delegated"); QTest::newRow("invited us3") << data_filename << action << receiver << incidenceUid << expectedResult << expectedNumIncidences << expectedPartStat; //---------------------------------------------------------------------------------------------- // Process a CANCEL without having the incidence in our calendar. // itiphandler should return success and not error expectedResult = ITIPHandler::ResultSuccess; data_filename = QStringLiteral("invited_us"); expectedNumIncidences = 0; action = QStringLiteral("cancel"); QTest::newRow("invited us4") << data_filename << action << receiver << incidenceUid << expectedResult << expectedNumIncidences << expectedPartStat; //---------------------------------------------------------------------------------------------- // Process a REQUEST without having the incidence in our calendar. // itiphandler should return success and add the rquest to a calendar expectedResult = ITIPHandler::ResultSuccess; data_filename = QLatin1String("invited_us"); expectedNumIncidences = 1; expectedPartStat = KCalCore::Attendee::NeedsAction; action = QLatin1String("request"); QTest::newRow("invited us5") << data_filename << action << receiver << incidenceUid << expectedResult << expectedNumIncidences << expectedPartStat; //---------------------------------------------------------------------------------------------- // Here we're testing an error case, where data is null. expectedResult = ITIPHandler::ResultError; expectedNumIncidences = 0; action = QStringLiteral("accepted"); QTest::newRow("invalid data") << QString() << action << receiver << incidenceUid << expectedResult << expectedNumIncidences << expectedPartStat; //---------------------------------------------------------------------------------------------- // Testing invalid action expectedResult = ITIPHandler::ResultError; data_filename = QStringLiteral("invitation_us"); expectedNumIncidences = 0; action = QStringLiteral("accepted"); QTest::newRow("invalid action") << data_filename << QString() << receiver << incidenceUid << expectedResult << expectedNumIncidences << expectedPartStat; //---------------------------------------------------------------------------------------------- // Test bug 235749 expectedResult = ITIPHandler::ResultSuccess; data_filename = QStringLiteral("bug235749"); expectedNumIncidences = 1; expectedPartStat = KCalCore::Attendee::Accepted; action = QStringLiteral("accepted"); incidenceUid = QStringLiteral("b6f0466a-8877-49d0-a4fc-8ee18ffd8e07"); // Don't change, hardcoded in data file QTest::newRow("bug 235749") << data_filename << action << receiver << incidenceUid << expectedResult << expectedNumIncidences << expectedPartStat; //---------------------------------------------------------------------------------------------- // Test counterproposal without a UI delegat set expectedResult = ITIPHandler::ResultError; data_filename = QStringLiteral("invited_us"); expectedNumIncidences = 0; expectedPartStat = KCalCore::Attendee::Accepted; action = QStringLiteral("counter"); incidenceUid = QStringLiteral("b6f0466a-8877-49d0-a4fc-8ee18ffd8e07"); QTest::newRow("counter error") << data_filename << action << receiver << incidenceUid << expectedResult << expectedNumIncidences << expectedPartStat; //---------------------------------------------------------------------------------------------- } void ITIPHandlerTest::testProcessITIPMessage() { QFETCH(QString, data_filename); QFETCH(QString, action); QFETCH(QString, receiver); QFETCH(QString, incidenceUid); QFETCH(Akonadi::ITIPHandler::Result, expectedResult); QFETCH(int, expectedNumIncidences); QFETCH(KCalCore::Attendee::PartStat, expectedPartStat); FakeMessageQueueJob::sUnitTestResults.clear(); createITIPHandler(); m_expectedResult = expectedResult; QString iCalData = icalData(data_filename); Akonadi::Item::List items; processItip(iCalData, receiver, action, expectedNumIncidences, items); if (expectedNumIncidences == 1) { KCalCore::Incidence::Ptr incidence = items.first().payload(); QVERIFY(incidence); QCOMPARE(incidence->schedulingID(), incidenceUid); QVERIFY(incidence->schedulingID() != incidence->uid()); KCalCore::Attendee::Ptr me = ourAttendee(incidence); QVERIFY(me); QCOMPARE(me->status(), expectedPartStat); } cleanup(); } void ITIPHandlerTest::testProcessITIPMessages_data() { QTest::addColumn("invitation_filenames"); // filename to create incidence (inputs) QTest::addColumn("expected_filename"); // filename with expected data (reference) QTest::addColumn("actions"); // we must specify the METHOD. This is an ITipHandler API workaround, not sure why we must pass it as argument since it's already inside the icaldata. QStringList invitation_filenames; QString expected_filename; QStringList actions; actions << QStringLiteral("accepted") << QStringLiteral("accepted"); //---------------------------------------------------------------------------------------------- // Someone invited us to an event, we accept, then organizer changes event, and we record update: invitation_filenames.clear(); invitation_filenames << QStringLiteral("invited_us") << QStringLiteral("invited_us_update01"); expected_filename = QStringLiteral("expected_data/update1"); QTest::newRow("accept update") << invitation_filenames << expected_filename << actions; //---------------------------------------------------------------------------------------------- // Someone invited us to an event, we accept, then organizer changes event, and we record update: invitation_filenames.clear(); invitation_filenames << QStringLiteral("invited_us") << QStringLiteral("invited_us_daily_update01"); expected_filename = QStringLiteral("expected_data/update2"); QTest::newRow("accept recurringupdate") << invitation_filenames << expected_filename << actions; //---------------------------------------------------------------------------------------------- // We accept a recurring event, then the organizer changes the summary to the second instance (RECID) expected_filename = QStringLiteral("expected_data/update3"); invitation_filenames.clear(); invitation_filenames << QStringLiteral("invited_us_daily") << QStringLiteral("invited_us_daily_update_recid01"); QTest::newRow("accept recid update") << invitation_filenames << expected_filename << actions; //---------------------------------------------------------------------------------------------- // We accept a recurring event, then we accept a CANCEL with recuring-id. // The result is that a exception with status CANCELLED should be created, and our main incidence // should not be touched invitation_filenames.clear(); invitation_filenames << QStringLiteral("invited_us_daily") << QStringLiteral("invited_us_daily_cancel_recid01"); expected_filename = QStringLiteral("expected_data/cancel1"); actions << QStringLiteral("accepted") << QStringLiteral("cancel"); QTest::newRow("accept recid cancel") << invitation_filenames << expected_filename << actions; //---------------------------------------------------------------------------------------------- } void ITIPHandlerTest::testProcessITIPMessages() { QFETCH(QStringList, invitation_filenames); QFETCH(QString, expected_filename); QFETCH(QStringList, actions); const QString receiver = QLatin1String(s_ourEmail); FakeMessageQueueJob::sUnitTestResults.clear(); createITIPHandler(); m_expectedResult = Akonadi::ITIPHandler::ResultSuccess; for (int i = 0; i < invitation_filenames.count(); i++) { // First accept the invitation that creates the incidence: QString iCalData = icalData(invitation_filenames.at(i)); Item::List items; qDebug() << "Processing " << invitation_filenames.at(i); processItip(iCalData, receiver, actions.at(i), -1, items); } QString expectedICalData = icalData(expected_filename); KCalCore::MemoryCalendar::Ptr expectedCalendar = KCalCore::MemoryCalendar::Ptr(new KCalCore::MemoryCalendar(KDateTime::UTC)); KCalCore::ICalFormat format; format.fromString(expectedCalendar, expectedICalData); compareCalendars(expectedCalendar); // Here's where the cool and complex comparations are done cleanup(); } void ITIPHandlerTest::testProcessITIPMessageCancel_data() { QTest::addColumn("creation_data_filename"); // filename to create incidence QTest::addColumn("cancel_data_filename"); // filename with incidence cancelation QTest::addColumn("incidenceUid"); // uid of incidence in invitation QString creation_data_filename; QString cancel_data_filename; QString incidenceUid = QStringLiteral("uosj936i6arrtl9c2i5r2mfuvg"); //---------------------------------------------------------------------------------------------- // Someone invited us to an event, we accept, then organizer cancels event creation_data_filename = QStringLiteral("invited_us"); cancel_data_filename = QStringLiteral("invited_us_cancel01"); QTest::newRow("cancel1") << creation_data_filename << cancel_data_filename << incidenceUid; //---------------------------------------------------------------------------------------------- // Someone invited us to daily event, we accept, then organizer cancels the whole recurrence series creation_data_filename = QStringLiteral("invited_us_daily"); cancel_data_filename = QStringLiteral("invited_us_daily_cancel01"); QTest::newRow("cancel_daily") << creation_data_filename << cancel_data_filename << incidenceUid; //---------------------------------------------------------------------------------------------- } void ITIPHandlerTest::testProcessITIPMessageCancel() { QFETCH(QString, creation_data_filename); QFETCH(QString, cancel_data_filename); QFETCH(QString, incidenceUid); const QString receiver = QLatin1String(s_ourEmail); FakeMessageQueueJob::sUnitTestResults.clear(); createITIPHandler(); m_expectedResult = Akonadi::ITIPHandler::ResultSuccess; // First accept the invitation that creates the incidence: QString iCalData = icalData(creation_data_filename); Item::List items; processItip(iCalData, receiver, QStringLiteral("accepted"), 1, items); KCalCore::Incidence::Ptr incidence = items.first().payload(); QVERIFY(incidence); // good, now accept the invitation that has the CANCEL iCalData = icalData(cancel_data_filename); processItip(iCalData, receiver, QStringLiteral("accepted"), 0, items); } void ITIPHandlerTest::testOutgoingInvitations_data() { QTest::addColumn("item"); // existing incidence that will be target of creation, deletion or modification QTest::addColumn("changeType"); // creation, deletion, modification QTest::addColumn("expectedEmailCount"); QTest::addColumn("invitationPolicy"); QTest::addColumn("userCancels"); const bool userDoesntCancel = false; const bool userCancels = true; Akonadi::Item item; KCalCore::Incidence::Ptr incidence; IncidenceChanger::ChangeType changeType; const IncidenceChanger::InvitationPolicy invitationPolicyAsk = IncidenceChanger::InvitationPolicyAsk; const IncidenceChanger::InvitationPolicy invitationPolicySend = IncidenceChanger::InvitationPolicySend; const IncidenceChanger::InvitationPolicy invitationPolicyDontSend = IncidenceChanger::InvitationPolicyDontSend; int expectedEmailCount = 0; Q_UNUSED(invitationPolicyAsk); const QString ourEmail = QLatin1String(s_ourEmail); const Attendee::Ptr us = Attendee::Ptr(new Attendee(QString(), ourEmail)); const Attendee::Ptr mia = Attendee::Ptr(new Attendee(QStringLiteral("Mia Wallace"), QStringLiteral("mia@dev.nul"))); const Attendee::Ptr vincent = Attendee::Ptr(new Attendee(QStringLiteral("Vincent"), QStringLiteral("vincent@dev.nul"))); const Attendee::Ptr jules = Attendee::Ptr(new Attendee(QStringLiteral("Jules"), QStringLiteral("jules@dev.nul"))); const QString uid = QStringLiteral("random-uid-123"); //---------------------------------------------------------------------------------------------- // Creation. We are organizer. We invite another person. changeType = IncidenceChanger::ChangeTypeCreate; item = generateIncidence(uid, /**organizer=*/ourEmail); incidence = item.payload(); incidence->addAttendee(vincent); incidence->addAttendee(jules); expectedEmailCount = 1; QTest::newRow("Creation. We organize.") << item << changeType << expectedEmailCount << invitationPolicySend << userDoesntCancel; //---------------------------------------------------------------------------------------------- // Creation. We are organizer. We invite another person. But we choose not to send invitation e-mail. changeType = IncidenceChanger::ChangeTypeCreate; item = generateIncidence(uid, /**organizer=*/ourEmail); incidence = item.payload(); incidence->addAttendee(vincent); incidence->addAttendee(jules); expectedEmailCount = 0; QTest::newRow("Creation. We organize.2") << item << changeType << expectedEmailCount << invitationPolicyDontSend << userDoesntCancel; //---------------------------------------------------------------------------------------------- // We delete an event that we organized, and has attendees, that will be notified. changeType = IncidenceChanger::ChangeTypeDelete; item = generateIncidence(uid, /**organizer=*/ourEmail); incidence = item.payload(); incidence->addAttendee(vincent); incidence->addAttendee(jules); expectedEmailCount = 1; QTest::newRow("Deletion. We organized.") << item << changeType << expectedEmailCount << invitationPolicySend << userDoesntCancel; //---------------------------------------------------------------------------------------------- // We delete an event that we organized, and has attendees. We won't send e-mail notifications. changeType = IncidenceChanger::ChangeTypeDelete; item = generateIncidence(uid, /**organizer=*/ourEmail); incidence = item.payload(); incidence->addAttendee(vincent); incidence->addAttendee(jules); expectedEmailCount = 0; QTest::newRow("Deletion. We organized.2") << item << changeType << expectedEmailCount << invitationPolicyDontSend << userDoesntCancel; //---------------------------------------------------------------------------------------------- // We delete an event that we organized, and has attendees, who will be notified. changeType = IncidenceChanger::ChangeTypeModify; item = generateIncidence(uid, /**organizer=*/ourEmail); incidence = item.payload(); incidence->addAttendee(vincent); incidence->addAttendee(jules); expectedEmailCount = 1; QTest::newRow("Modification. We organizd.") << item << changeType << expectedEmailCount << invitationPolicySend << userDoesntCancel; //---------------------------------------------------------------------------------------------- // We delete an event that we organized, and has attendees, who wont be notified. changeType = IncidenceChanger::ChangeTypeModify; item = generateIncidence(uid, /**organizer=*/ourEmail); incidence = item.payload(); incidence->addAttendee(vincent); // TODO: test that all attendees got the e-mail incidence->addAttendee(jules); expectedEmailCount = 0; QTest::newRow("Modification. We organizd.2") << item << changeType << expectedEmailCount << invitationPolicyDontSend << userDoesntCancel; //---------------------------------------------------------------------------------------------- // We delete an event which we're not the organizer of. Organizer gets REPLY with PartState=Declined changeType = IncidenceChanger::ChangeTypeDelete; item = generateIncidence(uid, /**organizer=*/mia->email()); incidence = item.payload(); incidence->addAttendee(vincent); incidence->addAttendee(jules); us->setStatus(Attendee::Accepted); // TODO: Test without accepted status incidence->addAttendee(us); // TODO: test that attendees didn't receive the REPLY expectedEmailCount = 1; // REPLY is always sent, there are no dialogs to control this. Dialogs only control REQUEST and CANCEL. Bug or feature ? QTest::newRow("Deletion. We didnt organize.") << item << changeType << expectedEmailCount << invitationPolicyDontSend << userDoesntCancel; //---------------------------------------------------------------------------------------------- // We delete an event which we're not the organizer of. Organizer gets REPLY with PartState=Declined changeType = IncidenceChanger::ChangeTypeDelete; item = generateIncidence(uid, /**organizer=*/mia->email()); incidence = item.payload(); incidence->addAttendee(vincent); incidence->addAttendee(jules); // TODO: test that attendees didn't receive the REPLY us->setStatus(Attendee::Accepted); // TODO: Test without accepted status incidence->addAttendee(us); expectedEmailCount = 1; QTest::newRow("Deletion. We didnt organize.2") << item << changeType << expectedEmailCount << invitationPolicySend << userDoesntCancel; //---------------------------------------------------------------------------------------------- // We modified an event which we're not the organizer of. And, when the "do you really want to modify", we choose "yes". changeType = IncidenceChanger::ChangeTypeModify; item = generateIncidence(uid, /**organizer=*/mia->email()); incidence = item.payload(); incidence->addAttendee(vincent); incidence->addAttendee(jules); us->setStatus(Attendee::Accepted); incidence->addAttendee(us); expectedEmailCount = 0; QTest::newRow("Modification. We didnt organize") << item << changeType << expectedEmailCount << invitationPolicySend << userDoesntCancel; //---------------------------------------------------------------------------------------------- // We modified an event which we're not the organizer of. And, when the "do you really want to modify", we choose "no". changeType = IncidenceChanger::ChangeTypeModify; item = generateIncidence(uid, /**organizer=*/mia->email()); incidence = item.payload(); incidence->addAttendee(vincent); incidence->addAttendee(jules); us->setStatus(Attendee::Accepted); incidence->addAttendee(us); expectedEmailCount = 0; QTest::newRow("Modification. We didnt organize.2") << item << changeType << expectedEmailCount << invitationPolicyDontSend << userCancels; //---------------------------------------------------------------------------------------------- } void ITIPHandlerTest::testOutgoingInvitations() { QFETCH(Akonadi::Item, item); QFETCH(IncidenceChanger::ChangeType, changeType); QFETCH(int, expectedEmailCount); QFETCH(IncidenceChanger::InvitationPolicy, invitationPolicy); QFETCH(bool, userCancels); KCalCore::Incidence::Ptr incidence = item.payload(); m_pendingIncidenceChangerSignal = 1; FakeMessageQueueJob::sUnitTestResults.clear(); m_changer->setInvitationPolicy(invitationPolicy); m_cancelExpected = userCancels; switch (changeType) { case IncidenceChanger::ChangeTypeCreate: m_changer->createIncidence(incidence, mCollection); waitForIt(); QCOMPARE(FakeMessageQueueJob::sUnitTestResults.count(), expectedEmailCount); break; case IncidenceChanger::ChangeTypeModify: { // Create if first, so we have something to modify m_changer->setGroupwareCommunication(false); // we disable groupware because creating an incidence which we're not the organizer of is not permitted. m_changer->createIncidence(incidence, mCollection); waitForIt(); m_changer->setGroupwareCommunication(true); QCOMPARE(FakeMessageQueueJob::sUnitTestResults.count(), 0); QVERIFY(mLastInsertedItem.isValid()); m_pendingIncidenceChangerSignal = 1; Incidence::Ptr oldIncidence = Incidence::Ptr(incidence->clone()); incidence->setSummary(QStringLiteral("the-new-summary")); int changeId = m_changer->modifyIncidence(mLastInsertedItem, oldIncidence); QVERIFY(changeId != 1); waitForIt(); QCOMPARE(FakeMessageQueueJob::sUnitTestResults.count(), expectedEmailCount); break; } case IncidenceChanger::ChangeTypeDelete: // Create if first, so we have something to delete m_changer->setGroupwareCommunication(false); m_changer->createIncidence(incidence, mCollection); waitForIt(); m_changer->setGroupwareCommunication(true); QCOMPARE(FakeMessageQueueJob::sUnitTestResults.count(), 0); QVERIFY(mLastInsertedItem.isValid()); m_pendingIncidenceChangerSignal = 1; m_changer->deleteIncidence(mLastInsertedItem); waitForIt(); QCOMPARE(FakeMessageQueueJob::sUnitTestResults.count(), expectedEmailCount); break; default: Q_ASSERT(false); } } void ITIPHandlerTest::testIdentity_data() { QTest::addColumn("email"); QTest::addColumn("expectedResult"); const QString myEmail = QLatin1String(s_ourEmail); QString myEmail2 = QStringLiteral("Some name <%1>").arg(myEmail); const QString myAlias1 = QStringLiteral("alias1@kde.org"); // hardcoded in emailidentities, do not change const QString myIdentity2 = QLatin1String(s_outEmail2); QTest::newRow("Me") << myEmail << true; QTest::newRow("Also me") << myEmail2 << true; QTest::newRow("My identity2") << myIdentity2 << true; QTest::newRow("Not me") << QStringLiteral("laura.palmer@twinpeaks.com") << false; QTest::newRow("My alias") << myAlias1 << true; } void ITIPHandlerTest::testIdentity() { QFETCH(QString, email); QFETCH(bool, expectedResult); if (CalendarUtils::thatIsMe(email) != expectedResult) { qDebug() << email; QVERIFY(false); } } void ITIPHandlerTest::cleanup() { Akonadi::Item::List items = calendarItems(); foreach (const Akonadi::Item &item, items) { ItemDeleteJob *job = new ItemDeleteJob(item); AKVERIFYEXEC(job); } delete m_itipHandler; m_itipHandler = 0; } void ITIPHandlerTest::createITIPHandler() { m_itipHandler = new Akonadi::ITIPHandler(new FakeITIPHandlerComponentFactory(this), this); m_itipHandler->setShowDialogsOnError(false); connect(m_itipHandler, SIGNAL(iTipMessageProcessed(Akonadi::ITIPHandler::Result,QString)), SLOT(oniTipMessageProcessed(Akonadi::ITIPHandler::Result,QString))); } QString ITIPHandlerTest::icalData(const QString &data_filename) { - QString absolutePath = QLatin1String(ITIP_DATA_DIR) + QLatin1Char('/') + data_filename; + QString absolutePath = QFINDTESTDATA(QStringLiteral("itip_data/") + data_filename); return QString::fromLatin1(readFile(absolutePath)); } void ITIPHandlerTest::processItip(const QString &icaldata, const QString &receiver, const QString &action, int expectedNumIncidences, Akonadi::Item::List &items) { items.clear(); m_pendingItipMessageSignal = 1; m_itipHandler->processiTIPMessage(receiver, icaldata, action); waitForIt(); // 0 e-mails are sent because the status update e-mail is sent by // kmail's text_calendar.cpp. QCOMPARE(FakeMessageQueueJob::sUnitTestResults.count(), 0); items = calendarItems(); if (expectedNumIncidences != -1) { QCOMPARE(items.count(), expectedNumIncidences); } } Attendee::Ptr ITIPHandlerTest::ourAttendee(const KCalCore::Incidence::Ptr &incidence) const { KCalCore::Attendee::List attendees = incidence->attendees(); KCalCore::Attendee::Ptr me; foreach (const KCalCore::Attendee::Ptr &attendee, attendees) { if (attendee->email() == QLatin1String(s_ourEmail)) { me = attendee; break; } } return me; } void ITIPHandlerTest::oniTipMessageProcessed(ITIPHandler::Result result, const QString &errorMessage) { if (result != ITIPHandler::ResultSuccess && result != m_expectedResult) { qDebug() << "ITIPHandlerTest::oniTipMessageProcessed() error = " << errorMessage; } m_pendingItipMessageSignal--; QVERIFY(m_pendingItipMessageSignal >= 0); if (m_pendingItipMessageSignal == 0) { stopWaiting(); } QCOMPARE(m_expectedResult, result); } void ITIPHandlerTest::onCreateFinished(int changeId, const Item &item, IncidenceChanger::ResultCode resultCode, const QString &errorString) { Q_UNUSED(changeId); Q_UNUSED(errorString); mLastInsertedItem = item; m_pendingIncidenceChangerSignal--; QVERIFY(m_pendingIncidenceChangerSignal >= 0); if (m_pendingIncidenceChangerSignal == 0) { stopWaiting(); } QCOMPARE(resultCode, IncidenceChanger::ResultCodeSuccess); } void ITIPHandlerTest::onDeleteFinished(int changeId, const QVector &deletedIds, IncidenceChanger::ResultCode resultCode, const QString &errorString) { Q_UNUSED(changeId); Q_UNUSED(errorString); Q_UNUSED(deletedIds); m_pendingIncidenceChangerSignal--; QVERIFY(m_pendingIncidenceChangerSignal >= 0); if (m_pendingIncidenceChangerSignal == 0) { stopWaiting(); } QCOMPARE(resultCode, IncidenceChanger::ResultCodeSuccess); } void ITIPHandlerTest::onModifyFinished(int changeId, const Item &item, IncidenceChanger::ResultCode resultCode, const QString &errorString) { Q_UNUSED(changeId); Q_UNUSED(errorString); Q_UNUSED(item); m_pendingIncidenceChangerSignal--; QVERIFY(m_pendingIncidenceChangerSignal >= 0); if (m_pendingIncidenceChangerSignal == 0) { stopWaiting(); } QCOMPARE(resultCode, m_cancelExpected ? IncidenceChanger::ResultCodeUserCanceled : IncidenceChanger::ResultCodeSuccess); } QTEST_AKONADIMAIN(ITIPHandlerTest) diff --git a/autotests/unittestbase.cpp b/autotests/unittestbase.cpp index d61674f..177d710 100644 --- a/autotests/unittestbase.cpp +++ b/autotests/unittestbase.cpp @@ -1,199 +1,200 @@ /* Copyright (c) 2013 Sérgio Martins 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 "unittestbase.h" #include "helper.h" #include "../src/fetchjobcalendar.h" #include "mailclient_p.h" #include #include #include #include #include #include #include #include #include #include #include using namespace Akonadi; using namespace KCalCore; UnitTestBase::UnitTestBase() { qRegisterMetaType("Akonadi::Item"); qRegisterMetaType >("QList"); qRegisterMetaType >("QVector"); qRegisterMetaType("Akonadi::MailClient::Result"); mChanger = new IncidenceChanger(this); mChanger->setShowDialogsOnError(false); mChanger->setHistoryEnabled(true); mCollection = Helper::fetchCollection(); Q_ASSERT(mCollection.isValid()); mChanger->setDefaultCollection(mCollection); } void UnitTestBase::waitForIt() { QTestEventLoop::instance().enterLoop(10); QVERIFY(!QTestEventLoop::instance().timeout()); } void UnitTestBase::stopWaiting() { QTestEventLoop::instance().exitLoop(); } void UnitTestBase::createIncidence(const QString &uid) { Item item = generateIncidence(uid); createIncidence(item); } void UnitTestBase::createIncidence(const Item &item) { QVERIFY(mCollection.isValid()); ItemCreateJob *job = new ItemCreateJob(item, mCollection, this); QVERIFY(job->exec()); } void UnitTestBase::verifyExists(const QString &uid, bool exists) { FetchJobCalendar *calendar = new FetchJobCalendar(); connect(calendar, &FetchJobCalendar::loadFinished, this, &UnitTestBase::onLoadFinished); waitForIt(); calendar->deleteLater(); QCOMPARE(calendar->incidence(uid) != 0, exists); } Akonadi::Item::List UnitTestBase::calendarItems() { FetchJobCalendar::Ptr calendar = FetchJobCalendar::Ptr(new FetchJobCalendar()); connect(calendar.data(), &FetchJobCalendar::loadFinished, this, &UnitTestBase::onLoadFinished); waitForIt(); KCalCore::ICalFormat format; QString dump = format.toString(calendar.staticCast()); qDebug() << dump; calendar->deleteLater(); return calendar->items(); } void UnitTestBase::onLoadFinished(bool success, const QString &) { QVERIFY(success); stopWaiting(); } void UnitTestBase::compareCalendars(const KCalCore::Calendar::Ptr &expectedCalendar) { FetchJobCalendar::Ptr calendar = FetchJobCalendar::Ptr(new FetchJobCalendar()); connect(calendar.data(), &FetchJobCalendar::loadFinished, this, &UnitTestBase::onLoadFinished); waitForIt(); // Now compare the expected calendar to the calendar we got. Incidence::List incidences = calendar->incidences(); Incidence::List expectedIncidences = expectedCalendar->incidences(); // First, replace the randomly generated UIDs with the UID that came in the invitation e-mail... foreach(const KCalCore::Incidence::Ptr &incidence, incidences) { incidence->setUid(incidence->schedulingID()); qDebug() << "We have incidece with uid=" << incidence->uid() << "; instanceidentifier=" << incidence->instanceIdentifier(); foreach(const KCalCore::Attendee::Ptr &attendee, incidence->attendees()) { attendee->setUid(attendee->email()); } } // ... so we can compare them foreach(const KCalCore::Incidence::Ptr &incidence, expectedIncidences) { incidence->setUid(incidence->schedulingID()); qDebug() << "We expect incidece with uid=" << incidence->uid() << "; instanceidentifier=" << incidence->instanceIdentifier(); foreach(const KCalCore::Attendee::Ptr &attendee, incidence->attendees()) { attendee->setUid(attendee->email()); } } QCOMPARE(incidences.count(), expectedIncidences.count()); foreach(const KCalCore::Incidence::Ptr &expectedIncidence, expectedIncidences) { KCalCore::Incidence::Ptr incidence; for (int i=0; iinstanceIdentifier() == expectedIncidence->instanceIdentifier()) { incidence = incidences.at(i); incidences.remove(i); break; } } QVERIFY(incidence); // Don't fail on creation times, which are obviously different expectedIncidence->setCreated(incidence->created()); incidence->removeCustomProperty(QByteArray("LIBKCAL"), QByteArray("ID")); if (*expectedIncidence != *incidence) { ICalFormat format; QString expectedData = format.toString(expectedIncidence); QString gotData = format.toString(incidence); qDebug() << "Test failed, expected:\n" << expectedData << "\nbut got " << gotData; QVERIFY(false); } } } /** static */ QByteArray UnitTestBase::readFile(const QString &filename) { QFile file(filename); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + qWarning() << "File could not be opened for reading:" << filename; return QByteArray(); } return file.readAll(); } Item UnitTestBase::generateIncidence(const QString &uid, const QString &organizer) { Item item; item.setMimeType(KCalCore::Event::eventMimeType()); KCalCore::Incidence::Ptr incidence = KCalCore::Incidence::Ptr(new KCalCore::Event()); if (!uid.isEmpty()) { incidence->setUid(uid); } const KDateTime now = KDateTime::currentUtcDateTime(); incidence->setDtStart(now); incidence->setDateTime(now.addSecs(3600), Incidence::RoleEnd); incidence->setSummary(QStringLiteral("summary")); item.setPayload(incidence); if (!organizer.isEmpty()) { incidence->setOrganizer(organizer); } return item; } diff --git a/autotests/unittestenv/config-sqlite-db.xml b/autotests/unittestenv/config-mysql-fs.xml similarity index 56% copy from autotests/unittestenv/config-sqlite-db.xml copy to autotests/unittestenv/config-mysql-fs.xml index 1ba6b3f..10513a1 100644 --- a/autotests/unittestenv/config-sqlite-db.xml +++ b/autotests/unittestenv/config-mysql-fs.xml @@ -1,8 +1,7 @@ - kdehome - xdgconfig-sqlite.db + xdgconfig-mysql.fs xdglocal akonadi_ical_resource true - sqlite + mysql diff --git a/autotests/unittestenv/config-sqlite-db.xml b/autotests/unittestenv/config-sqlite-db.xml index 1ba6b3f..f93366f 100644 --- a/autotests/unittestenv/config-sqlite-db.xml +++ b/autotests/unittestenv/config-sqlite-db.xml @@ -1,8 +1,7 @@ - kdehome xdgconfig-sqlite.db xdglocal akonadi_ical_resource true sqlite diff --git a/autotests/unittestenv/config.xml b/autotests/unittestenv/config.xml new file mode 100644 index 0000000..f19b28a --- /dev/null +++ b/autotests/unittestenv/config.xml @@ -0,0 +1,5 @@ + + xdgconfig + xdglocal + + diff --git a/autotests/unittestenv/kdehome/share/config/akonadi_ical_resource_0rc b/autotests/unittestenv/kdehome/share/config/akonadi_ical_resource_0rc deleted file mode 100644 index ba9e269..0000000 --- a/autotests/unittestenv/kdehome/share/config/akonadi_ical_resource_0rc +++ /dev/null @@ -1,2 +0,0 @@ -[General] -Path[$e]=$KDEHOME/std0.ics \ No newline at end of file diff --git a/autotests/unittestenv/kdehome/testdata.xml b/autotests/unittestenv/kdehome/testdata.xml deleted file mode 100644 index e69de29..0000000 diff --git a/autotests/unittestenv/xdgconfig-mysql.fs/akonadi/akonadiserverrc b/autotests/unittestenv/xdgconfig-mysql.fs/akonadi/akonadiserverrc new file mode 100644 index 0000000..33e7f81 --- /dev/null +++ b/autotests/unittestenv/xdgconfig-mysql.fs/akonadi/akonadiserverrc @@ -0,0 +1,10 @@ +[%General] +# This is a slightly adjusted version of the QSQLITE driver from Qt +# It is provided by akonadi itself +Driver=QSQLITE3 + +[Debug] +Tracer=null + +[Search] +Manager=Dummy diff --git a/autotests/unittestenv/kdehome/share/config/akonadi-firstrunrc b/autotests/unittestenv/xdgconfig/akonadi-firstrunrc similarity index 100% rename from autotests/unittestenv/kdehome/share/config/akonadi-firstrunrc rename to autotests/unittestenv/xdgconfig/akonadi-firstrunrc diff --git a/autotests/unittestenv/xdgconfig/akonadi_ical_resource_0rc b/autotests/unittestenv/xdgconfig/akonadi_ical_resource_0rc new file mode 100644 index 0000000..f2f711c --- /dev/null +++ b/autotests/unittestenv/xdgconfig/akonadi_ical_resource_0rc @@ -0,0 +1,2 @@ +[General] +Path[$e]=$XDG_DATA_HOME/std0.ics diff --git a/autotests/unittestenv/kdehome/share/config/emaildefaults b/autotests/unittestenv/xdgconfig/emaildefaults similarity index 100% rename from autotests/unittestenv/kdehome/share/config/emaildefaults rename to autotests/unittestenv/xdgconfig/emaildefaults diff --git a/autotests/unittestenv/kdehome/share/config/emailidentities b/autotests/unittestenv/xdgconfig/emailidentities similarity index 100% rename from autotests/unittestenv/kdehome/share/config/emailidentities rename to autotests/unittestenv/xdgconfig/emailidentities diff --git a/autotests/unittestenv/kdehome/share/config/mailtransports b/autotests/unittestenv/xdgconfig/mailtransports similarity index 100% rename from autotests/unittestenv/kdehome/share/config/mailtransports rename to autotests/unittestenv/xdgconfig/mailtransports diff --git a/autotests/unittestenv/kdehome/std0.ics b/autotests/unittestenv/xdglocal/std0.ics similarity index 100% rename from autotests/unittestenv/kdehome/std0.ics rename to autotests/unittestenv/xdglocal/std0.ics diff --git a/src/incidencechanger.cpp b/src/incidencechanger.cpp index cf3a0b0..4de4a08 100644 --- a/src/incidencechanger.cpp +++ b/src/incidencechanger.cpp @@ -1,1425 +1,1422 @@ /* Copyright (C) 2004 Reinhold Kainhofer Copyright (C) 2010-2012 Sérgio Martins 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 "incidencechanger.h" #include "incidencechanger_p.h" #include "mailscheduler_p.h" #include "utils_p.h" #include "akonadicalendar_debug.h" #include #include #include #include #include #include #include #include #include using namespace Akonadi; using namespace KCalCore; -#ifdef PLEASE_TEST_INVITATIONS -# define RUNNING_UNIT_TESTS true -#else -# define RUNNING_UNIT_TESTS false -#endif +AKONADI_CALENDAR_TESTS_EXPORT bool akonadi_calendar_running_unittests = false; + static ITIPHandlerDialogDelegate::Action actionFromStatus(ITIPHandlerHelper::SendResult result) { //enum SendResult { // Canceled, /**< Sending was canceled by the user, meaning there are // local changes of which other attendees are not aware. */ // FailKeepUpdate, /**< Sending failed, the changes to the incidence must be kept. */ // FailAbortUpdate, /**< Sending failed, the changes to the incidence must be undone. */ // NoSendingNeeded, /**< In some cases it is not needed to send an invitation // (e.g. when we are the only attendee) */ // Success switch (result) { case ITIPHandlerHelper::ResultCanceled: return ITIPHandlerDialogDelegate::ActionDontSendMessage; case ITIPHandlerHelper::ResultSuccess: return ITIPHandlerDialogDelegate::ActionSendMessage; default: return ITIPHandlerDialogDelegate::ActionAsk; } } static bool weAreOrganizer(const Incidence::Ptr &incidence) { const QString email = incidence->organizer()->email(); return Akonadi::CalendarUtils::thatIsMe(email); } static bool allowedModificationsWithoutRevisionUpdate(const Incidence::Ptr &incidence) { // Modifications that are per user allowd without getting outofsync with organisator // * if only alarm settings are modified. const QSet dirtyFields = incidence->dirtyFields(); QSet alarmOnlyModify; alarmOnlyModify << IncidenceBase::FieldAlarms << IncidenceBase::FieldLastModified; return (dirtyFields == alarmOnlyModify); } namespace Akonadi { // Does a queued emit, with QMetaObject::invokeMethod static void emitCreateFinished(IncidenceChanger *changer, int changeId, const Akonadi::Item &item, Akonadi::IncidenceChanger::ResultCode resultCode, const QString &errorString) { QMetaObject::invokeMethod(changer, "createFinished", Qt::QueuedConnection, Q_ARG(int, changeId), Q_ARG(Akonadi::Item, item), Q_ARG(Akonadi::IncidenceChanger::ResultCode, resultCode), Q_ARG(QString, errorString)); } // Does a queued emit, with QMetaObject::invokeMethod static void emitModifyFinished(IncidenceChanger *changer, int changeId, const Akonadi::Item &item, IncidenceChanger::ResultCode resultCode, const QString &errorString) { QMetaObject::invokeMethod(changer, "modifyFinished", Qt::QueuedConnection, Q_ARG(int, changeId), Q_ARG(Akonadi::Item, item), Q_ARG(Akonadi::IncidenceChanger::ResultCode, resultCode), Q_ARG(QString, errorString)); } // Does a queued emit, with QMetaObject::invokeMethod static void emitDeleteFinished(IncidenceChanger *changer, int changeId, const QVector &itemIdList, IncidenceChanger::ResultCode resultCode, const QString &errorString) { QMetaObject::invokeMethod(changer, "deleteFinished", Qt::QueuedConnection, Q_ARG(int, changeId), Q_ARG(QVector, itemIdList), Q_ARG(Akonadi::IncidenceChanger::ResultCode, resultCode), Q_ARG(QString, errorString)); } } typedef QHash IdToRevisionHash; Q_GLOBAL_STATIC(IdToRevisionHash, s_latestRevisionByItemId) IncidenceChanger::Private::Private(bool enableHistory, ITIPHandlerComponentFactory *factory, IncidenceChanger *qq) : q(qq) { mLatestChangeId = 0; mShowDialogsOnError = true; mFactory = factory ? factory : new ITIPHandlerComponentFactory(this); mHistory = enableHistory ? new History(this) : Q_NULLPTR; mUseHistory = enableHistory; mDestinationPolicy = DestinationPolicyDefault; mRespectsCollectionRights = false; mGroupwareCommunication = false; mLatestAtomicOperationId = 0; mBatchOperationInProgress = false; mAutoAdjustRecurrence = true; m_collectionFetchJob = Q_NULLPTR; m_invitationPolicy = InvitationPolicyAsk; qRegisterMetaType >("QVector"); qRegisterMetaType("Akonadi::Item::Id"); qRegisterMetaType("Akonadi::Item"); qRegisterMetaType( "Akonadi::IncidenceChanger::ResultCode"); qRegisterMetaType("ITIPHandlerHelper::SendResult"); } IncidenceChanger::Private::~Private() { if (!mAtomicOperations.isEmpty() || !mQueuedModifications.isEmpty() || !mModificationsInProgress.isEmpty()) { qCDebug(AKONADICALENDAR_LOG) << "Normal if the application was being used. " "But might indicate a memory leak if it wasn't"; } } bool IncidenceChanger::Private::atomicOperationIsValid(uint atomicOperationId) const { // Changes must be done between startAtomicOperation() and endAtomicOperation() return mAtomicOperations.contains(atomicOperationId) && !mAtomicOperations[atomicOperationId]->m_endCalled; } bool IncidenceChanger::Private::hasRights(const Collection &collection, IncidenceChanger::ChangeType changeType) const { bool result = false; switch (changeType) { case ChangeTypeCreate: result = collection.rights() & Akonadi::Collection::CanCreateItem; break; case ChangeTypeModify: result = collection.rights() & Akonadi::Collection::CanChangeItem; break; case ChangeTypeDelete: result = collection.rights() & Akonadi::Collection::CanDeleteItem; break; default: Q_ASSERT_X(false, "hasRights", "invalid type"); } return !collection.isValid() || !mRespectsCollectionRights || result; } Akonadi::Job *IncidenceChanger::Private::parentJob(const Change::Ptr &change) const { return (mBatchOperationInProgress && !change->queuedModification) ? mAtomicOperations[mLatestAtomicOperationId]->transaction() : Q_NULLPTR; } void IncidenceChanger::Private::queueModification(const Change::Ptr &change) { // If there's already a change queued we just discard it // and send the newer change, which already includes // previous modifications const Akonadi::Item::Id id = change->newItem.id(); if (mQueuedModifications.contains(id)) { Change::Ptr toBeDiscarded = mQueuedModifications.take(id); toBeDiscarded->resultCode = ResultCodeModificationDiscarded; toBeDiscarded->completed = true; mChangeById.remove(toBeDiscarded->id); } change->queuedModification = true; mQueuedModifications[id] = change; } void IncidenceChanger::Private::performNextModification(Akonadi::Item::Id id) { mModificationsInProgress.remove(id); if (mQueuedModifications.contains(id)) { const Change::Ptr change = mQueuedModifications.take(id); performModification(change); } } void IncidenceChanger::Private::handleTransactionJobResult(KJob *job) { TransactionSequence *transaction = qobject_cast(job); Q_ASSERT(transaction); Q_ASSERT(mAtomicOperationByTransaction.contains(transaction)); const uint atomicOperationId = mAtomicOperationByTransaction.take(transaction); Q_ASSERT(mAtomicOperations.contains(atomicOperationId)); AtomicOperation *operation = mAtomicOperations[atomicOperationId]; Q_ASSERT(operation); Q_ASSERT(operation->m_id == atomicOperationId); if (job->error()) { if (!operation->rolledback()) { operation->setRolledback(); } qCritical() << "Transaction failed, everything was rolledback. " << job->errorString(); } else { Q_ASSERT(operation->m_endCalled); Q_ASSERT(!operation->pendingJobs()); } if (!operation->pendingJobs() && operation->m_endCalled) { delete mAtomicOperations.take(atomicOperationId); mBatchOperationInProgress = false; } else { operation->m_transactionCompleted = true; } } void IncidenceChanger::Private::handleCreateJobResult(KJob *job) { Change::Ptr change = mChangeForJob.take(job); const ItemCreateJob *j = qobject_cast(job); Q_ASSERT(j); Akonadi::Item item = j->item(); if (j->error()) { const QString errorString = j->errorString(); ResultCode resultCode = ResultCodeJobError; item = change->newItem; qCritical() << errorString; if (mShowDialogsOnError) { KMessageBox::sorry(change->parentWidget, i18n("Error while trying to create calendar item. Error was: %1", errorString)); } mChangeById.remove(change->id); change->errorString = errorString; change->resultCode = resultCode; // puff, change finally goes out of scope, and emits the incidenceCreated signal. } else { Q_ASSERT(item.isValid()); Q_ASSERT(item.hasPayload()); change->newItem = item; if (change->useGroupwareCommunication) { connect(change.data(), &Change::dialogClosedAfterChange, this, &Private::handleCreateJobResult2); handleInvitationsAfterChange(change); } else { handleCreateJobResult2(change->id, ITIPHandlerHelper::ResultSuccess); } } } void IncidenceChanger::Private::handleCreateJobResult2(int changeId, ITIPHandlerHelper::SendResult status) { Change::Ptr change = mChangeById[changeId]; Akonadi::Item item = change->newItem; mChangeById.remove(changeId); if (status == ITIPHandlerHelper::ResultFailAbortUpdate) { qCritical() << "Sending invitations failed, but did not delete the incidence"; } const uint atomicOperationId = change->atomicOperationId; if (atomicOperationId != 0) { mInvitationStatusByAtomicOperation.insert(atomicOperationId, status); } QString description; if (change->atomicOperationId != 0) { AtomicOperation *a = mAtomicOperations[change->atomicOperationId]; ++a->m_numCompletedChanges; change->completed = true; description = a->m_description; } // for user undo/redo if (change->recordToHistory) { mHistory->recordCreation(item, description, change->atomicOperationId); } change->errorString = QString(); change->resultCode = ResultCodeSuccess; // puff, change finally goes out of scope, and emits the incidenceCreated signal. } void IncidenceChanger::Private::handleDeleteJobResult(KJob *job) { Change::Ptr change = mChangeForJob.take(job); const ItemDeleteJob *j = qobject_cast(job); const Item::List items = j->deletedItems(); QSharedPointer deletionChange = change.staticCast(); deletionChange->mItemIds.reserve(deletionChange->mItemIds.count() + items.count()); foreach (const Akonadi::Item &item, items) { deletionChange->mItemIds.append(item.id()); } QString description; if (change->atomicOperationId != 0) { AtomicOperation *a = mAtomicOperations[change->atomicOperationId]; a->m_numCompletedChanges++; change->completed = true; description = a->m_description; } if (j->error()) { const QString errorString = j->errorString(); qCritical() << errorString; if (mShowDialogsOnError) { KMessageBox::sorry(change->parentWidget, i18n("Error while trying to delete calendar item. Error was: %1", errorString)); } foreach (const Item &item, items) { // Werent deleted due to error mDeletedItemIds.remove(mDeletedItemIds.indexOf(item.id())); } mChangeById.remove(change->id); change->resultCode = ResultCodeJobError; change->errorString = errorString; change->emitCompletionSignal(); } else { // success if (change->recordToHistory) { Q_ASSERT(mHistory); mHistory->recordDeletions(items, description, change->atomicOperationId); } if (change->useGroupwareCommunication) { connect(change.data(), &Change::dialogClosedAfterChange, this, &Private::handleDeleteJobResult2); handleInvitationsAfterChange(change); } else { handleDeleteJobResult2(change->id, ITIPHandlerHelper::ResultSuccess); } } } void IncidenceChanger::Private::handleDeleteJobResult2(int changeId, ITIPHandlerHelper::SendResult status) { Change::Ptr change = mChangeById[changeId]; mChangeById.remove(change->id); if (status == ITIPHandlerHelper::ResultSuccess) { change->errorString = QString(); change->resultCode = ResultCodeSuccess; } else { change->errorString = i18nc("errormessage for a job ended with an unexpected result", "An unknown error occurred"); change->resultCode = ResultCodeJobError; } // puff, change finally goes out of scope, and emits the incidenceDeleted signal. } void IncidenceChanger::Private::handleModifyJobResult(KJob *job) { Change::Ptr change = mChangeForJob.take(job); const ItemModifyJob *j = qobject_cast(job); const Item item = j->item(); Q_ASSERT(mDirtyFieldsByJob.contains(job)); Q_ASSERT(item.hasPayload()); const QSet dirtyFields = mDirtyFieldsByJob.value(job); item.payload()->setDirtyFields(dirtyFields); QString description; if (change->atomicOperationId != 0) { AtomicOperation *a = mAtomicOperations[change->atomicOperationId]; a->m_numCompletedChanges++; change->completed = true; description = a->m_description; } if (j->error()) { const QString errorString = j->errorString(); ResultCode resultCode = ResultCodeJobError; if (deleteAlreadyCalled(item.id())) { // User deleted the item almost at the same time he changed it. We could just return success // but the delete is probably already recorded to History, and that would make undo not work // in the proper order. resultCode = ResultCodeAlreadyDeleted; qCWarning(AKONADICALENDAR_LOG) << "Trying to change item " << item.id() << " while deletion is in progress."; } else { qCritical() << errorString; } if (mShowDialogsOnError) { KMessageBox::sorry(change->parentWidget, i18n("Error while trying to modify calendar item. Error was: %1", errorString)); } mChangeById.remove(change->id); change->errorString = errorString; change->resultCode = resultCode; // puff, change finally goes out of scope, and emits the incidenceModified signal. QMetaObject::invokeMethod(this, "performNextModification", Qt::QueuedConnection, Q_ARG(Akonadi::Item::Id, item.id())); } else { // success (*(s_latestRevisionByItemId()))[item.id()] = item.revision(); change->newItem = item; if (change->recordToHistory && !change->originalItems.isEmpty()) { Q_ASSERT(change->originalItems.count() == 1); mHistory->recordModification(change->originalItems.first(), item, description, change->atomicOperationId); } if (change->useGroupwareCommunication) { connect(change.data(), &Change::dialogClosedAfterChange, this, &Private::handleModifyJobResult2); handleInvitationsAfterChange(change); } else { handleModifyJobResult2(change->id, ITIPHandlerHelper::ResultSuccess); } } } void IncidenceChanger::Private::handleModifyJobResult2(int changeId, ITIPHandlerHelper::SendResult status) { Change::Ptr change = mChangeById[changeId]; mChangeById.remove(changeId); if (change->atomicOperationId != 0) { mInvitationStatusByAtomicOperation.insert(change->atomicOperationId, status); } change->errorString = QString(); change->resultCode = ResultCodeSuccess; // puff, change finally goes out of scope, and emits the incidenceModified signal. QMetaObject::invokeMethod(this, "performNextModification", Qt::QueuedConnection, Q_ARG(Akonadi::Item::Id, change->newItem.id())); } bool IncidenceChanger::Private::deleteAlreadyCalled(Akonadi::Item::Id id) const { return mDeletedItemIds.contains(id); } void IncidenceChanger::Private::handleInvitationsBeforeChange(const Change::Ptr &change) { if (mGroupwareCommunication) { ITIPHandlerHelper::SendResult result = ITIPHandlerHelper::ResultSuccess; switch (change->type) { case IncidenceChanger::ChangeTypeCreate: // nothing needs to be done break; case IncidenceChanger::ChangeTypeDelete: { ITIPHandlerHelper::SendResult status; bool sendOk = true; Q_ASSERT(!change->originalItems.isEmpty()); ITIPHandlerHelper *handler = new ITIPHandlerHelper(mFactory, change->parentWidget); handler->setParent(this); if (m_invitationPolicy == InvitationPolicySend) { handler->setDefaultAction(ITIPHandlerDialogDelegate::ActionSendMessage); } else if (m_invitationPolicy == InvitationPolicyDontSend) { handler->setDefaultAction(ITIPHandlerDialogDelegate::ActionDontSendMessage); } else if (mInvitationStatusByAtomicOperation.contains(change->atomicOperationId)) { handler->setDefaultAction(actionFromStatus(mInvitationStatusByAtomicOperation.value(change->atomicOperationId))); } connect(handler, &ITIPHandlerHelper::finished, change.data(), &Change::emitUserDialogClosedBeforeChange); foreach (const Akonadi::Item &item, change->originalItems) { Q_ASSERT(item.hasPayload()); Incidence::Ptr incidence = CalendarUtils::incidence(item); if (!incidence->supportsGroupwareCommunication()) { continue; } // We only send CANCEL if we're the organizer. // If we're not, then we send REPLY with PartStat=Declined in handleInvitationsAfterChange() if (Akonadi::CalendarUtils::thatIsMe(incidence->organizer()->email())) { //TODO: not to popup all delete message dialogs at once :( sendOk = false; handler->sendIncidenceDeletedMessage(KCalCore::iTIPCancel, incidence); if (change->atomicOperationId) { mInvitationStatusByAtomicOperation.insert(change->atomicOperationId, status); } //TODO: with some status we want to break immediately } } if (sendOk) { change->emitUserDialogClosedBeforeChange(result); } return; } case IncidenceChanger::ChangeTypeModify: { if (change->originalItems.isEmpty()) { break; } Q_ASSERT(change->originalItems.count() == 1); Incidence::Ptr oldIncidence = CalendarUtils::incidence(change->originalItems.first()); Incidence::Ptr newIncidence = CalendarUtils::incidence(change->newItem); if (!oldIncidence->supportsGroupwareCommunication()) { break; } if (allowedModificationsWithoutRevisionUpdate(newIncidence)) { change->emitUserDialogClosedBeforeChange(ITIPHandlerHelper::ResultSuccess); return; } - if (RUNNING_UNIT_TESTS && !weAreOrganizer(newIncidence)) { + if (akonadi_calendar_running_unittests && !weAreOrganizer(newIncidence)) { // This is a bit of a workaround when running tests. I don't want to show the // "You're not organizer, do you want to modify event?" dialog in unit-tests, but want // to emulate a "yes" and a "no" press. if (m_invitationPolicy == InvitationPolicySend) { change->emitUserDialogClosedBeforeChange(ITIPHandlerHelper::ResultSuccess); return; } else if (m_invitationPolicy == InvitationPolicyDontSend) { change->emitUserDialogClosedBeforeChange(ITIPHandlerHelper::ResultCanceled); return; } } ITIPHandlerHelper handler(mFactory, change->parentWidget); const bool modify = handler.handleIncidenceAboutToBeModified(newIncidence); if (modify) { break; } else { result = ITIPHandlerHelper::ResultCanceled; } if (newIncidence->type() == oldIncidence->type()) { IncidenceBase *i1 = newIncidence.data(); IncidenceBase *i2 = oldIncidence.data(); *i1 = *i2; } break; } default: Q_ASSERT(false); result = ITIPHandlerHelper::ResultCanceled; } change->emitUserDialogClosedBeforeChange(result); } else { change->emitUserDialogClosedBeforeChange(ITIPHandlerHelper::ResultSuccess); } } void IncidenceChanger::Private::handleInvitationsAfterChange(const Change::Ptr &change) { if (change->useGroupwareCommunication) { ITIPHandlerHelper *handler = new ITIPHandlerHelper(mFactory, change->parentWidget); connect(handler, &ITIPHandlerHelper::finished, change.data(), &Change::emitUserDialogClosedAfterChange); handler->setParent(this); const bool alwaysSend = m_invitationPolicy == InvitationPolicySend; const bool neverSend = m_invitationPolicy == InvitationPolicyDontSend; if (alwaysSend) { handler->setDefaultAction(ITIPHandlerDialogDelegate::ActionSendMessage); } if (neverSend) { handler->setDefaultAction(ITIPHandlerDialogDelegate::ActionDontSendMessage); } switch (change->type) { case IncidenceChanger::ChangeTypeCreate: { Incidence::Ptr incidence = CalendarUtils::incidence(change->newItem); if (incidence->supportsGroupwareCommunication()) { handler->sendIncidenceCreatedMessage(KCalCore::iTIPRequest, incidence); return; } break; } case IncidenceChanger::ChangeTypeDelete: { handler->deleteLater(); handler = Q_NULLPTR; Q_ASSERT(!change->originalItems.isEmpty()); foreach (const Akonadi::Item &item, change->originalItems) { Q_ASSERT(item.hasPayload()); Incidence::Ptr incidence = CalendarUtils::incidence(item); Q_ASSERT(incidence); if (!incidence->supportsGroupwareCommunication()) { continue; } if (!Akonadi::CalendarUtils::thatIsMe(incidence->organizer()->email())) { const QStringList myEmails = Akonadi::CalendarUtils::allEmails(); bool notifyOrganizer = false; KCalCore::Attendee::Ptr me(incidence->attendeeByMails(myEmails)); if (me) { if (me->status() == KCalCore::Attendee::Accepted || me->status() == KCalCore::Attendee::Delegated) { notifyOrganizer = true; } KCalCore::Attendee::Ptr newMe(new KCalCore::Attendee(*me)); newMe->setStatus(KCalCore::Attendee::Declined); incidence->clearAttendees(); incidence->addAttendee(newMe); //break; } if (notifyOrganizer) { MailScheduler scheduler(mFactory, change->parentWidget); // TODO make async scheduler.performTransaction(incidence, KCalCore::iTIPReply); } } } break; } case IncidenceChanger::ChangeTypeModify: { if (change->originalItems.isEmpty()) { break; } Q_ASSERT(change->originalItems.count() == 1); Incidence::Ptr oldIncidence = CalendarUtils::incidence(change->originalItems.first()); Incidence::Ptr newIncidence = CalendarUtils::incidence(change->newItem); if (!newIncidence->supportsGroupwareCommunication() || !Akonadi::CalendarUtils::thatIsMe(newIncidence->organizer()->email())) { // If we're not the organizer, the user already saw the "Do you really want to do this, incidence will become out of sync" break; } if (allowedModificationsWithoutRevisionUpdate(newIncidence)) { break; } if (!neverSend && !alwaysSend && mInvitationStatusByAtomicOperation.contains(change->atomicOperationId)) { handler->setDefaultAction(actionFromStatus(mInvitationStatusByAtomicOperation.value(change->atomicOperationId))); } const bool attendeeStatusChanged = myAttendeeStatusChanged(newIncidence, oldIncidence, Akonadi::CalendarUtils::allEmails()); handler->sendIncidenceModifiedMessage(KCalCore::iTIPRequest, newIncidence, attendeeStatusChanged); return; break; } default: handler->deleteLater(); handler = Q_NULLPTR; Q_ASSERT(false); change->emitUserDialogClosedAfterChange(ITIPHandlerHelper::ResultCanceled); return; } handler->deleteLater(); handler = Q_NULLPTR; change->emitUserDialogClosedAfterChange(ITIPHandlerHelper::ResultSuccess); } else { change->emitUserDialogClosedAfterChange(ITIPHandlerHelper::ResultSuccess); } } /** static */ bool IncidenceChanger::Private::myAttendeeStatusChanged(const Incidence::Ptr &newInc, const Incidence::Ptr &oldInc, const QStringList &myEmails) { Q_ASSERT(newInc); Q_ASSERT(oldInc); const Attendee::Ptr oldMe = oldInc->attendeeByMails(myEmails); const Attendee::Ptr newMe = newInc->attendeeByMails(myEmails); return oldMe && newMe && oldMe->status() != newMe->status(); } IncidenceChanger::IncidenceChanger(QObject *parent) : QObject(parent) , d(new Private(/**history=*/true, /*factory=*/Q_NULLPTR, this)) { } IncidenceChanger::IncidenceChanger(ITIPHandlerComponentFactory *factory, QObject *parent) : QObject(parent) , d(new Private(/**history=*/true, factory, this)) { } IncidenceChanger::IncidenceChanger(bool enableHistory, QObject *parent) : QObject(parent) , d(new Private(enableHistory, /*factory=*/Q_NULLPTR, this)) { } IncidenceChanger::~IncidenceChanger() { delete d; } int IncidenceChanger::createIncidence(const Incidence::Ptr &incidence, const Collection &collection, QWidget *parent) { if (!incidence) { qCWarning(AKONADICALENDAR_LOG) << "An invalid payload is not allowed."; d->cancelTransaction(); return -1; } const uint atomicOperationId = d->mBatchOperationInProgress ? d->mLatestAtomicOperationId : 0; const Change::Ptr change(new CreationChange(this, ++d->mLatestChangeId, atomicOperationId, parent)); const int changeId = change->id; Q_ASSERT(!(d->mBatchOperationInProgress && !d->mAtomicOperations.contains(atomicOperationId))); if (d->mBatchOperationInProgress && d->mAtomicOperations[atomicOperationId]->rolledback()) { const QString errorMessage = d->showErrorDialog(ResultCodeRolledback, parent); qCWarning(AKONADICALENDAR_LOG) << errorMessage; change->resultCode = ResultCodeRolledback; change->errorString = errorMessage; d->cleanupTransaction(); return changeId; } Item item; item.setPayload(incidence); item.setMimeType(incidence->mimeType()); change->newItem = item; d->step1DetermineDestinationCollection(change, collection); return change->id; } int IncidenceChanger::deleteIncidence(const Item &item, QWidget *parent) { Item::List list; list.append(item); return deleteIncidences(list, parent); } int IncidenceChanger::deleteIncidences(const Item::List &items, QWidget *parent) { if (items.isEmpty()) { qCritical() << "Delete what?"; d->cancelTransaction(); return -1; } foreach (const Item &item, items) { if (!item.isValid()) { qCritical() << "Items must be valid!"; d->cancelTransaction(); return -1; } } const uint atomicOperationId = d->mBatchOperationInProgress ? d->mLatestAtomicOperationId : 0; const int changeId = ++d->mLatestChangeId; const Change::Ptr change(new DeletionChange(this, changeId, atomicOperationId, parent)); foreach (const Item &item, items) { if (!d->hasRights(item.parentCollection(), ChangeTypeDelete)) { qCWarning(AKONADICALENDAR_LOG) << "Item " << item.id() << " can't be deleted due to ACL restrictions"; const QString errorString = d->showErrorDialog(ResultCodePermissions, parent); change->resultCode = ResultCodePermissions; change->errorString = errorString; d->cancelTransaction(); return changeId; } } if (!d->allowAtomicOperation(atomicOperationId, change)) { const QString errorString = d->showErrorDialog(ResultCodeDuplicateId, parent); change->resultCode = ResultCodeDuplicateId; change->errorString = errorString; qCWarning(AKONADICALENDAR_LOG) << errorString; d->cancelTransaction(); return changeId; } Item::List itemsToDelete; foreach (const Item &item, items) { if (d->deleteAlreadyCalled(item.id())) { // IncidenceChanger::deleteIncidence() called twice, ignore this one. qCDebug(AKONADICALENDAR_LOG) << "Item " << item.id() << " already deleted or being deleted, skipping"; } else { itemsToDelete.append(item); } } if (d->mBatchOperationInProgress && d->mAtomicOperations[atomicOperationId]->rolledback()) { const QString errorMessage = d->showErrorDialog(ResultCodeRolledback, parent); change->resultCode = ResultCodeRolledback; change->errorString = errorMessage; qCritical() << errorMessage; d->cleanupTransaction(); return changeId; } if (itemsToDelete.isEmpty()) { QVector itemIdList; itemIdList.append(Item().id()); qCDebug(AKONADICALENDAR_LOG) << "Items already deleted or being deleted, skipping"; const QString errorMessage = i18n("That calendar item was already deleted, or currently being deleted."); // Queued emit because return must be executed first, otherwise caller won't know this workId change->resultCode = ResultCodeAlreadyDeleted; change->errorString = errorMessage; d->cancelTransaction(); qCWarning(AKONADICALENDAR_LOG) << errorMessage; return changeId; } change->originalItems = itemsToDelete; d->mChangeById.insert(changeId, change); if (d->mGroupwareCommunication) { connect(change.data(), &Change::dialogClosedBeforeChange, d, &Private::deleteIncidences2); d->handleInvitationsBeforeChange(change); } else { d->deleteIncidences2(changeId, ITIPHandlerHelper::ResultSuccess); } return changeId; } void IncidenceChanger::Private::deleteIncidences2(int changeId, ITIPHandlerHelper::SendResult status) { Q_UNUSED(status); Change::Ptr change = mChangeById[changeId]; const uint atomicOperationId = change->atomicOperationId; ItemDeleteJob *deleteJob = new ItemDeleteJob(change->originalItems, parentJob(change)); mChangeForJob.insert(deleteJob, change); if (mBatchOperationInProgress) { AtomicOperation *atomic = mAtomicOperations[atomicOperationId]; Q_ASSERT(atomic); atomic->addChange(change); } mDeletedItemIds.reserve(mDeletedItemIds.count() + change->originalItems.count()); foreach (const Item &item, change->originalItems) { mDeletedItemIds << item.id(); } // Do some cleanup if (mDeletedItemIds.count() > 100) { mDeletedItemIds.remove(0, 50); } // QueuedConnection because of possible sync exec calls. connect(deleteJob, &KJob::result, this, &Private::handleDeleteJobResult, Qt::QueuedConnection); } int IncidenceChanger::modifyIncidence(const Item &changedItem, const KCalCore::Incidence::Ptr &originalPayload, QWidget *parent) { if (!changedItem.isValid() || !changedItem.hasPayload()) { qCWarning(AKONADICALENDAR_LOG) << "An invalid item or payload is not allowed."; d->cancelTransaction(); return -1; } if (!d->hasRights(changedItem.parentCollection(), ChangeTypeModify)) { qCWarning(AKONADICALENDAR_LOG) << "Item " << changedItem.id() << " can't be deleted due to ACL restrictions"; const int changeId = ++d->mLatestChangeId; const QString errorString = d->showErrorDialog(ResultCodePermissions, parent); emitModifyFinished(this, changeId, changedItem, ResultCodePermissions, errorString); d->cancelTransaction(); return changeId; } //TODO also update revision here instead of in the editor changedItem.payload()->setLastModified(KDateTime::currentUtcDateTime()); const uint atomicOperationId = d->mBatchOperationInProgress ? d->mLatestAtomicOperationId : 0; const int changeId = ++d->mLatestChangeId; ModificationChange *modificationChange = new ModificationChange(this, changeId, atomicOperationId, parent); Change::Ptr change(modificationChange); if (originalPayload) { Item originalItem(changedItem); originalItem.setPayload(originalPayload); modificationChange->originalItems << originalItem; } modificationChange->newItem = changedItem; d->mChangeById.insert(changeId, change); if (!d->allowAtomicOperation(atomicOperationId, change)) { const QString errorString = d->showErrorDialog(ResultCodeDuplicateId, parent); change->resultCode = ResultCodeDuplicateId; change->errorString = errorString; d->cancelTransaction(); qCWarning(AKONADICALENDAR_LOG) << "Atomic operation now allowed"; return changeId; } if (d->mBatchOperationInProgress && d->mAtomicOperations[atomicOperationId]->rolledback()) { const QString errorMessage = d->showErrorDialog(ResultCodeRolledback, parent); qCritical() << errorMessage; d->cleanupTransaction(); emitModifyFinished(this, changeId, changedItem, ResultCodeRolledback, errorMessage); } else { d->adjustRecurrence(originalPayload, CalendarUtils::incidence(modificationChange->newItem)); d->performModification(change); } return changeId; } void IncidenceChanger::Private::performModification(const Change::Ptr &change) { const Item::Id id = change->newItem.id(); Akonadi::Item &newItem = change->newItem; Q_ASSERT(newItem.isValid()); Q_ASSERT(newItem.hasPayload()); const int changeId = change->id; if (deleteAlreadyCalled(id)) { // IncidenceChanger::deleteIncidence() called twice, ignore this one. qCDebug(AKONADICALENDAR_LOG) << "Item " << id << " already deleted or being deleted, skipping"; // Queued emit because return must be executed first, otherwise caller won't know this workId emitModifyFinished(q, change->id, newItem, ResultCodeAlreadyDeleted, i18n("That calendar item was already deleted, or currently being deleted.")); return; } const uint atomicOperationId = change->atomicOperationId; const bool hasAtomicOperationId = atomicOperationId != 0; if (hasAtomicOperationId && mAtomicOperations[atomicOperationId]->rolledback()) { const QString errorMessage = showErrorDialog(ResultCodeRolledback, Q_NULLPTR); qCritical() << errorMessage; emitModifyFinished(q, changeId, newItem, ResultCodeRolledback, errorMessage); return; } if (mGroupwareCommunication) { connect(change.data(), &Change::dialogClosedBeforeChange, this, &Private::performModification2); handleInvitationsBeforeChange(change); } else { performModification2(change->id, ITIPHandlerHelper::ResultSuccess); } } void IncidenceChanger::Private::performModification2(int changeId, ITIPHandlerHelper::SendResult status) { Change::Ptr change = mChangeById[changeId]; const Item::Id id = change->newItem.id(); Akonadi::Item &newItem = change->newItem; Q_ASSERT(newItem.isValid()); Q_ASSERT(newItem.hasPayload()); if (status == ITIPHandlerHelper::ResultCanceled) { //TODO:fireout what is right here:) // User got a "You're not the organizer, do you really want to send" dialog, and said "no" qCDebug(AKONADICALENDAR_LOG) << "User cancelled, giving up"; emitModifyFinished(q, change->id, newItem, ResultCodeUserCanceled, QString()); return; } const uint atomicOperationId = change->atomicOperationId; const bool hasAtomicOperationId = atomicOperationId != 0; QHash &latestRevisionByItemId = *(s_latestRevisionByItemId()); if (latestRevisionByItemId.contains(id) && latestRevisionByItemId[id] > newItem.revision()) { /* When a ItemModifyJob ends, the application can still modify the old items if the user * is quick because the ETM wasn't updated yet, and we'll get a STORE error, because * we are not modifying the latest revision. * * When a job ends, we keep the new revision in s_latestRevisionByItemId * so we can update the item's revision */ newItem.setRevision(latestRevisionByItemId[id]); } Incidence::Ptr incidence = CalendarUtils::incidence(newItem); { if (!allowedModificationsWithoutRevisionUpdate(incidence)) { // increment revision ( KCalCore revision, not akonadi ) const int revision = incidence->revision(); incidence->setRevision(revision + 1); } //Reset attendee status, when resceduling QSet resetPartStatus; resetPartStatus << IncidenceBase::FieldDtStart << IncidenceBase::FieldDtEnd << IncidenceBase::FieldDtStart << IncidenceBase::FieldLocation << IncidenceBase::FieldDtDue << IncidenceBase::FieldDuration << IncidenceBase::FieldRecurrence; if (!(incidence->dirtyFields() & resetPartStatus).isEmpty() && weAreOrganizer(incidence)) { foreach (const Attendee::Ptr &attendee, incidence->attendees()) { if (attendee->role() != Attendee::NonParticipant && attendee->status() != Attendee::Delegated && !Akonadi::CalendarUtils::thatIsMe(attendee)) { attendee->setStatus(Attendee::NeedsAction); attendee->setRSVP(true); } } } } // Dav Fix // Don't write back remote revision since we can't make sure it is the current one newItem.setRemoteRevision(QString()); if (mModificationsInProgress.contains(newItem.id())) { // There's already a ItemModifyJob running for this item ID // Let's wait for it to end. queueModification(change); } else { ItemModifyJob *modifyJob = new ItemModifyJob(newItem, parentJob(change)); mChangeForJob.insert(modifyJob, change); mDirtyFieldsByJob.insert(modifyJob, incidence->dirtyFields()); if (hasAtomicOperationId) { AtomicOperation *atomic = mAtomicOperations[atomicOperationId]; Q_ASSERT(atomic); atomic->addChange(change); } mModificationsInProgress[newItem.id()] = change; // QueuedConnection because of possible sync exec calls. connect(modifyJob, &KJob::result, this, &Private::handleModifyJobResult, Qt::QueuedConnection); } } void IncidenceChanger::startAtomicOperation(const QString &operationDescription) { if (d->mBatchOperationInProgress) { qCDebug(AKONADICALENDAR_LOG) << "An atomic operation is already in progress."; return; } ++d->mLatestAtomicOperationId; d->mBatchOperationInProgress = true; AtomicOperation *atomicOperation = new AtomicOperation(d, d->mLatestAtomicOperationId); atomicOperation->m_description = operationDescription; d->mAtomicOperations.insert(d->mLatestAtomicOperationId, atomicOperation); } void IncidenceChanger::endAtomicOperation() { if (!d->mBatchOperationInProgress) { qCDebug(AKONADICALENDAR_LOG) << "No atomic operation is in progress."; return; } Q_ASSERT_X(d->mLatestAtomicOperationId != 0, "IncidenceChanger::endAtomicOperation()", "Call startAtomicOperation() first."); Q_ASSERT(d->mAtomicOperations.contains(d->mLatestAtomicOperationId)); AtomicOperation *atomicOperation = d->mAtomicOperations[d->mLatestAtomicOperationId]; Q_ASSERT(atomicOperation); atomicOperation->m_endCalled = true; const bool allJobsCompleted = !atomicOperation->pendingJobs(); if (allJobsCompleted && atomicOperation->rolledback() && atomicOperation->m_transactionCompleted) { // The transaction job already completed, we can cleanup: delete d->mAtomicOperations.take(d->mLatestAtomicOperationId); d->mBatchOperationInProgress = false; }/* else if ( allJobsCompleted ) { Q_ASSERT( atomicOperation->transaction ); atomicOperation->transaction->commit(); we using autocommit now }*/ } void IncidenceChanger::setShowDialogsOnError(bool enable) { d->mShowDialogsOnError = enable; } bool IncidenceChanger::showDialogsOnError() const { return d->mShowDialogsOnError; } void IncidenceChanger::setRespectsCollectionRights(bool respects) { d->mRespectsCollectionRights = respects; } bool IncidenceChanger::respectsCollectionRights() const { return d->mRespectsCollectionRights; } void IncidenceChanger::setDestinationPolicy(IncidenceChanger::DestinationPolicy destinationPolicy) { d->mDestinationPolicy = destinationPolicy; } IncidenceChanger::DestinationPolicy IncidenceChanger::destinationPolicy() const { return d->mDestinationPolicy; } void IncidenceChanger::setDefaultCollection(const Akonadi::Collection &collection) { d->mDefaultCollection = collection; } Collection IncidenceChanger::defaultCollection() const { return d->mDefaultCollection; } bool IncidenceChanger::historyEnabled() const { return d->mUseHistory; } void IncidenceChanger::setHistoryEnabled(bool enable) { if (d->mUseHistory != enable) { d->mUseHistory = enable; if (enable && !d->mHistory) { d->mHistory = new History(d); } } } History *IncidenceChanger::history() const { return d->mHistory; } bool IncidenceChanger::deletedRecently(Akonadi::Item::Id id) const { return d->deleteAlreadyCalled(id); } void IncidenceChanger::setGroupwareCommunication(bool enabled) { d->mGroupwareCommunication = enabled; } bool IncidenceChanger::groupwareCommunication() const { return d->mGroupwareCommunication; } void IncidenceChanger::setAutoAdjustRecurrence(bool enable) { d->mAutoAdjustRecurrence = enable; } bool IncidenceChanger::autoAdjustRecurrence() const { return d->mAutoAdjustRecurrence; } void IncidenceChanger::setInvitationPolicy(IncidenceChanger::InvitationPolicy policy) { d->m_invitationPolicy = policy; } IncidenceChanger::InvitationPolicy IncidenceChanger::invitationPolicy() const { return d->m_invitationPolicy; } Akonadi::Collection IncidenceChanger::lastCollectionUsed() const { return d->mLastCollectionUsed; } QString IncidenceChanger::Private::showErrorDialog(IncidenceChanger::ResultCode resultCode, QWidget *parent) { QString errorString; switch (resultCode) { case IncidenceChanger::ResultCodePermissions: errorString = i18n("Operation can not be performed due to ACL restrictions"); break; case IncidenceChanger::ResultCodeInvalidUserCollection: errorString = i18n("The chosen collection is invalid"); break; case IncidenceChanger::ResultCodeInvalidDefaultCollection: errorString = i18n("Default collection is invalid or doesn't have proper ACLs" " and DestinationPolicyNeverAsk was used"); break; case IncidenceChanger::ResultCodeDuplicateId: errorString = i18n("Duplicate item id in a group operation"); break; case IncidenceChanger::ResultCodeRolledback: errorString = i18n("One change belonging to a group of changes failed. " "All changes are being rolled back."); break; default: Q_ASSERT(false); return QString(i18n("Unknown error")); } if (mShowDialogsOnError) { KMessageBox::sorry(parent, errorString); } return errorString; } void IncidenceChanger::Private::adjustRecurrence(const KCalCore::Incidence::Ptr &originalIncidence, const KCalCore::Incidence::Ptr &incidence) { if (!originalIncidence || !incidence->recurs() || incidence->hasRecurrenceId() || !mAutoAdjustRecurrence || !incidence->dirtyFields().contains(KCalCore::Incidence::FieldDtStart)) { return; } const QDate originalDate = originalIncidence->dtStart().date(); const QDate newStartDate = incidence->dtStart().date(); if (!originalDate.isValid() || !newStartDate.isValid() || originalDate == newStartDate) { return; } KCalCore::Recurrence *recurrence = incidence->recurrence(); switch (recurrence->recurrenceType()) { case KCalCore::Recurrence::rWeekly: { QBitArray days = recurrence->days(); const int oldIndex = originalDate.dayOfWeek() - 1; // QDate returns [1-7]; const int newIndex = newStartDate.dayOfWeek() - 1; if (oldIndex != newIndex) { days.clearBit(oldIndex); days.setBit(newIndex); recurrence->setWeekly(recurrence->frequency(), days); } } default: break; // Other types not implemented } // Now fix cases where dtstart would be bigger than the recurrence end rendering it impossible for a view to show it: // To retrieve the recurrence end don't trust Recurrence::endDt() since it returns dtStart if the rrule's end is < than dtstart, // it seems someone made Recurrence::endDt() more robust, but getNextOccurrences() still craps out. So lets fix it here // there's no reason to write bogus ical to disk. const QDate recurrenceEndDate = recurrence->defaultRRule() ? recurrence->defaultRRule()->endDt().date() : QDate(); if (recurrenceEndDate.isValid() && recurrenceEndDate < newStartDate) { recurrence->setEndDate(newStartDate); } } void IncidenceChanger::Private::cancelTransaction() { if (mBatchOperationInProgress) { mAtomicOperations[mLatestAtomicOperationId]->setRolledback(); } } void IncidenceChanger::Private::cleanupTransaction() { Q_ASSERT(mAtomicOperations.contains(mLatestAtomicOperationId)); AtomicOperation *operation = mAtomicOperations[mLatestAtomicOperationId]; Q_ASSERT(operation); Q_ASSERT(operation->rolledback()); if (!operation->pendingJobs() && operation->m_endCalled && operation->m_transactionCompleted) { delete mAtomicOperations.take(mLatestAtomicOperationId); mBatchOperationInProgress = false; } } bool IncidenceChanger::Private::allowAtomicOperation(int atomicOperationId, const Change::Ptr &change) const { bool allow = true; if (atomicOperationId > 0) { Q_ASSERT(mAtomicOperations.contains(atomicOperationId)); AtomicOperation *operation = mAtomicOperations.value(atomicOperationId); if (change->type == ChangeTypeCreate) { allow = true; } else if (change->type == ChangeTypeModify) { allow = !operation->m_itemIdsInOperation.contains(change->newItem.id()); } else if (change->type == ChangeTypeDelete) { DeletionChange::Ptr deletion = change.staticCast(); foreach (Akonadi::Item::Id id, deletion->mItemIds) { if (operation->m_itemIdsInOperation.contains(id)) { allow = false; break; } } } } if (!allow) { qCWarning(AKONADICALENDAR_LOG) << "Each change belonging to a group operation" << "must have a different Akonadi::Item::Id"; } return allow; } /**reimp*/ void ModificationChange::emitCompletionSignal() { emitModifyFinished(changer, id, newItem, resultCode, errorString); } /**reimp*/ void CreationChange::emitCompletionSignal() { // Does a queued emit, with QMetaObject::invokeMethod emitCreateFinished(changer, id, newItem, resultCode, errorString); } /**reimp*/ void DeletionChange::emitCompletionSignal() { emitDeleteFinished(changer, id, mItemIds, resultCode, errorString); } /** Lost code from KDE 4.4 that was never called/used with incidenceeditors-ng. Attendees were removed from this incidence. Only the removed attendees are present in the incidence, so we just need to send a cancel messages to all attendees groupware messages are enabled at all. void IncidenceChanger::cancelAttendees( const Akonadi::Item &aitem ) { const KCalCore::Incidence::Ptr incidence = CalendarSupport::incidence( aitem ); Q_ASSERT( incidence ); if ( KCalPrefs::instance()->mUseGroupwareCommunication ) { if ( KMessageBox::questionYesNo( 0, i18n( "Some attendees were removed from the incidence. " "Shall cancel messages be sent to these attendees?" ), i18n( "Attendees Removed" ), KGuiItem( i18n( "Send Messages" ) ), KGuiItem( i18n( "Do Not Send" ) ) ) == KMessageBox::Yes ) { // don't use Akonadi::Groupware::sendICalMessage here, because that asks just // a very general question "Other people are involved, send message to // them?", which isn't helpful at all in this situation. Afterwards, it // would only call the Akonadi::MailScheduler::performTransaction, so do this // manually. CalendarSupport::MailScheduler scheduler( static_cast(d->mCalendar) ); scheduler.performTransaction( incidence, KCalCore::iTIPCancel ); } } } */ AtomicOperation::AtomicOperation(IncidenceChanger::Private *icp, uint ident) : m_id(ident) , m_endCalled(false) , m_numCompletedChanges(0) , m_transactionCompleted(false) , m_wasRolledback(false) , m_transaction(Q_NULLPTR) , m_incidenceChangerPrivate(icp) { Q_ASSERT(m_id != 0); } Akonadi::TransactionSequence *AtomicOperation::transaction() { if (!m_transaction) { m_transaction = new Akonadi::TransactionSequence; m_transaction->setAutomaticCommittingEnabled(true); m_incidenceChangerPrivate->mAtomicOperationByTransaction.insert(m_transaction, m_id); QObject::connect(m_transaction, &KJob::result, m_incidenceChangerPrivate, &IncidenceChanger::Private::handleTransactionJobResult); } return m_transaction; } diff --git a/src/mailclient_p.h b/src/mailclient_p.h index d9e90d3..6fea856 100644 --- a/src/mailclient_p.h +++ b/src/mailclient_p.h @@ -1,133 +1,128 @@ /* Copyright (c) 1998 Barry D Benowitz Copyright (c) 2001 Cornelius Schumacher Copyright (c) 2009 Allen Winter 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 AKONADI_MAILCLIENT_P_H #define AKONADI_MAILCLIENT_P_H -#include "akonadi-calendar_export.h" +#include "akonadi-calendar_tests_export.h" #include "itiphandler.h" #include #include #include struct UnitTestResult { typedef QList List; QString from; QStringList to; QStringList cc; QStringList bcc; int transportId; KMime::Message::Ptr message; UnitTestResult() : transportId(-1) { } }; namespace KIdentityManagement { class Identity; } class KJob; namespace Akonadi { -#ifdef PLEASE_TEST_INVITATIONS -#define EXPORT_MAILCLIENT AKONADI_CALENDAR_EXPORT -#else -#define EXPORT_MAILCLIENT -#endif class ITIPHandlerComponentFactory; -class EXPORT_MAILCLIENT MailClient : public QObject +class AKONADI_CALENDAR_TESTS_EXPORT MailClient : public QObject { Q_OBJECT public: enum Result { ResultSuccess, ResultNoAttendees, ResultReallyNoAttendees, ResultErrorCreatingTransport, ResultErrorFetchingTransport, ResultQueueJobError }; explicit MailClient(ITIPHandlerComponentFactory *factory, QObject *parent = Q_NULLPTR); ~MailClient(); void mailAttendees(const KCalCore::IncidenceBase::Ptr &incidence, const KIdentityManagement::Identity &identity, bool bccMe, const QString &attachment = QString(), const QString &mailTransport = QString()); void mailOrganizer(const KCalCore::IncidenceBase::Ptr &incidence, const KIdentityManagement::Identity &identity, const QString &from, bool bccMe, const QString &attachment = QString(), const QString &sub = QString(), const QString &mailTransport = QString()); void mailTo(const KCalCore::IncidenceBase::Ptr &incidence, const KIdentityManagement::Identity &identity, const QString &from, bool bccMe, const QString &recipients, const QString &attachment = QString(), const QString &mailTransport = QString()); /** Sends mail with specified from, to and subject field and body as text. If bcc is set, send a blind carbon copy to the sender @param incidence is the incidence, that is sended @param identity is the Identity of the sender @param from is the address of the sender of the message @param to a list of addresses to receive the message @param cc a list of addresses to receive message carbon copies @param subject is the subject of the message @param body is the boody of the message @param hidden if true and using KMail as the mailer, send the message without opening a composer window. @param bcc if true, send a blind carbon copy to the message sender @param attachment optional attachment (raw data) @param mailTransport defines the mail transport method. See here the kdepimlibs/mailtransport library. */ void send(const KCalCore::IncidenceBase::Ptr &incidence, const KIdentityManagement::Identity &identity, const QString &from, const QString &to, const QString &cc, const QString &subject, const QString &body, bool hidden = false, bool bccMe = false, const QString &attachment = QString(), const QString &mailTransport = QString()); private: void handleQueueJobFinished(KJob *job); Q_SIGNALS: void finished(Akonadi::MailClient::Result result, const QString &errorString); public: // For unit-test usage, since we can't depend on kdepim-runtime on jenkins ITIPHandlerComponentFactory *mFactory; }; } Q_DECLARE_METATYPE(Akonadi::MailClient::Result) #endif