diff --git a/CMakeLists.txt b/CMakeLists.txt index a9e0d3cd..c8da9a89 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,185 +1,185 @@ cmake_minimum_required(VERSION 3.5) set(PIM_VERSION "5.11.40") project(kdepim-addons VERSION ${PIM_VERSION}) if(POLICY CMP0063) cmake_policy(SET CMP0063 NEW) endif() set(KF5_MIN_VERSION "5.59.0") find_package(ECM ${KF5_MIN_VERSION} CONFIG REQUIRED) set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules/ ${ECM_MODULE_PATH}) # Do NOT add quote set(KDEPIM_DEV_VERSION alpha) # add an extra space if(DEFINED KDEPIM_DEV_VERSION) set(KDEPIM_DEV_VERSION " ${KDEPIM_DEV_VERSION}") endif() set(KDEPIMADDONS_VERSION_NUMBER ${PIM_VERSION}) set(KDEPIMADDONS_VERSION "${KDEPIMADDONS_VERSION_NUMBER}${KDEPIM_DEV_VERSION}") set(KDEPIMADDONS_LIB_VERSION "${KDEPIMADDONS_VERSION_NUMBER}") set(KDEPIMADDONS_LIB_SOVERSION "5") set(AKONADINOTES_LIB_VERSION "5.11.40") set(QT_REQUIRED_VERSION "5.11.0") include(ECMInstallIcons) include(ECMSetupVersion) include(ECMAddTests) include(GenerateExportHeader) include(ECMGenerateHeaders) include(FeatureSummary) include(CheckFunctionExists) include(ECMGeneratePriFile) include(KDEInstallDirs) include(KDECMakeSettings) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(ECMAddAppIcon) include(ECMQtDeclareLoggingCategory) option(KDEPIMADDONS_BUILD_EXAMPLES "Build the kdepim-addons example applications." TRUE) option(KDEPIM_ENTERPRISE_BUILD "Enable features specific to the enterprise branch, which are normally disabled. Also, it disables many components not needed for Kontact such as the Kolab client." FALSE) option(KMAIL_EDITORCONVERTERPLUGIN_TEMPLATE_BUILD "Build the kmail editor converter plugin." FALSE) option(KMAIL_DKIM_CONFIGURE_DIALOG_BUILD "Build dkim-verify configure dialog (experimental)." FALSE) find_package(Qt5 ${QT_REQUIRED_VERSION} CONFIG REQUIRED WebEngine WebEngineWidgets Widgets Test) find_package(KF5I18n ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5Config ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5DBusAddons ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5AkonadiNotes ${AKONADINOTES_LIB_VERSION} CONFIG REQUIRED) find_package(KF5XmlGui ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5Declarative ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5SyntaxHighlighting ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5KIO ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5IconThemes ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5Parts ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5Prison ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5Holidays ${KF5_MIN_VERSION} CONFIG REQUIRED) set(MAILCOMMON_LIB_VERSION "5.11.42") set(GRAVATAR_LIB_VERSION "5.11.40") set(PIMCOMMON_LIB_VERSION "5.11.40") set(GRANTLEETHEME_LIB_VERSION "5.11.40") set(CALENDARSUPPORT_LIB_VERSION "5.11.40") set(EVENTVIEW_LIB_VERSION "5.11.40") set(LIBKDEPIM_LIB_VERSION "5.11.40") set(KDEPIM_APPS_LIB_VERSION "5.11.40") set(LIBKLEO_LIB_VERSION "5.11.40") set(AKONADI_LIB_VERSION "5.11.40") set(INCIDENCEEDITOR_LIB_VERSION "5.11.40") set(KTNEF_LIB_VERSION "5.11.40") set(MESSAGELIB_LIB_VERSION "5.11.44") set(AKONADICALENDAR_LIB_VERSION "5.11.40") set(CALENDAR_UTILS_VERSION "5.11.40") set(KPIMTEXTEDIT_LIB_VERSION "5.11.41") set(KIMAP_LIB_VERSION "5.11.40") set(LIBKSIEVE_LIB_VERSION "5.11.40") set(KMAILTRANSPORT_LIB_VERSION "5.11.40") set(AKONADICONTACT_LIB_VERSION "5.11.40") set(KCONTACTS_LIB_VERSION "5.11.44") set(IMPORTWIZARD_LIB_VERSION "5.11.40") set(MAILIMPORTER_LIB_VERSION "5.11.40") set(KPIMPKPASS_LIB_VERSION "5.11.40") set(KPIMITINERARY_LIB_VERSION "5.11.40") -set(KCALENDARCORE_LIB_VERSION "5.11.44") +set(KCALENDARCORE_LIB_VERSION "5.11.45") find_package(KF5CalendarCore ${KCALENDARCORE_LIB_VERSION} CONFIG REQUIRED) find_package(KF5CalendarUtils ${CALENDAR_UTILS_VERSION} CONFIG REQUIRED) find_package(KF5WebEngineViewer ${MESSAGELIB_LIB_VERSION} CONFIG REQUIRED) find_package(KF5TemplateParser ${MESSAGELIB_LIB_VERSION} CONFIG REQUIRED) find_package(KF5MailCommon ${MAILCOMMON_LIB_VERSION} CONFIG REQUIRED) find_package(KF5KaddressbookGrantlee ${KDEPIM_APPS_LIB_VERSION} CONFIG REQUIRED) find_package(KF5MessageViewer ${MESSAGELIB_LIB_VERSION} CONFIG REQUIRED) find_package(KF5KaddressbookImportExport ${KDEPIM_APPS_LIB_VERSION} CONFIG REQUIRED) find_package(KF5Libkleo ${LIBKLEO_LIB_VERSION} CONFIG REQUIRED) find_package(KF5GrantleeTheme ${GRANTLEETHEME_LIB_VERSION} CONFIG REQUIRED) find_package(KF5PimCommonAkonadi ${PIMCOMMON_LIB_VERSION} CONFIG REQUIRED) find_package(KF5LibkdepimAkonadi ${LIBKDEPIM_LIB_VERSION} CONFIG REQUIRED) find_package(KF5IncidenceEditor ${INCIDENCEEDITOR_LIB_VERSION} CONFIG REQUIRED) find_package(KF5MessageCore ${MESSAGELIB_LIB_VERSION} CONFIG REQUIRED) find_package(KF5MessageComposer ${MESSAGELIB_LIB_VERSION} CONFIG REQUIRED) find_package(KF5MessageList ${MESSAGELIB_LIB_VERSION} CONFIG REQUIRED) find_package(KF5CalendarSupport ${CALENDARSUPPORT_LIB_VERSION} CONFIG REQUIRED) find_package(KF5EventViews ${EVENTVIEW_LIB_VERSION} CONFIG REQUIRED) find_package(KF5Akonadi ${AKONADI_LIB_VERSION} CONFIG REQUIRED) find_package(KF5AkonadiCalendar ${AKONADICALENDAR_LIB_VERSION} CONFIG REQUIRED) find_package(KF5Gravatar ${GRAVATAR_LIB_VERSION} CONFIG REQUIRED) find_package(KF5PimTextEdit ${KPIMTEXTEDIT_LIB_VERSION} CONFIG REQUIRED) find_package(KF5IdentityManagement ${KIDENTITYMANAGEMENT_LIB_VERSION} CONFIG REQUIRED) find_package(KF5IMAP ${KIMAP_LIB_VERSION} CONFIG REQUIRED) find_package(KF5LibKSieve ${LIBKSIEVE_LIB_VERSION} CONFIG REQUIRED) find_package(KF5Tnef ${KTNEF_LIB_VERSION} CONFIG REQUIRED) find_package(KF5MailTransportAkonadi ${KMAILTRANSPORT_LIB_VERSION} CONFIG REQUIRED) find_package(KF5Contacts ${KCONTACTS_LIB_VERSION} CONFIG REQUIRED) find_package(KF5AkonadiContact ${AKONADICONTACT_LIB_VERSION} CONFIG REQUIRED) find_package(KF5ContactEditor ${AKONADICONTACT_LIB_VERSION} CONFIG REQUIRED) find_package(KPimImportWizard ${IMPORTWIZARD_LIB_VERSION} CONFIG) find_package(KF5MailImporterAkonadi ${MAILIMPORTER_LIB_VERSION} CONFIG REQUIRED) find_package(KPimPkPass ${KPIMPKPASS_LIB_VERSION} CONFIG REQUIRED) find_package(KPimItinerary ${KPIMITINERARY_LIB_VERSION} CONFIG REQUIRED) set(CMAKE_CXX_STANDARD 14) if(BUILD_TESTING) add_definitions(-DBUILD_TESTING) endif(BUILD_TESTING) if(KDEPIM_ENTERPRISE_BUILD) set(KDEPIM_ENTERPRISE_BUILD true) else() set(KDEPIM_ENTERPRISE_BUILD false) endif() # Extra package find_package(Gpgmepp 1.11.1 CONFIG) set_package_properties(Gpgmepp PROPERTIES DESCRIPTION "GpgME library" URL "https://www.gnupg.org" TYPE REQUIRED) add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x060000) add_definitions(-DQT_NO_FOREACH) option(KDEPIM_RUN_AKONADI_TEST "Enable autotest based on Akonadi." TRUE) if(KDEPIMADDONS_BUILD_EXAMPLES) add_subdirectory(examples) endif() include_directories(${CMAKE_CURRENT_SOURCE_DIR}) set(QTCREATOR_TEMPLATE_INSTALL_DIR ${KDE_INSTALL_DATADIR}/qtcreator/templates CACHE PATH "Define qtcreator template install path (default is /usr/share/qtcreator/templates)") find_package(Discount "2") set_package_properties("discount" PROPERTIES DESCRIPTION "A library that gives you formatting functions suitable for marking down entire documents or lines of text" URL "https://www.pell.portland.or.us/~orc/Code/discount/" TYPE RECOMMENDED PURPOSE "Generate Markdown file.") if(discount_FOUND) if (${PC_LIBMARKDOWN_VERSION_STRING}) if (${PC_LIBMARKDOWN_VERSION_STRING} VERSION_GREATER "2.2.5") set(DISCOUNT_HAS_HIGHLIGHTING_SUPPORT TRUE) MESSAGE(STATUS "Discount ${PC_LIBMARKDOWN_VERSION_STRING}: has hightlighting support") endif() endif() endif() add_subdirectory(plugins) add_subdirectory(korganizer) add_subdirectory(kmail) add_subdirectory(kaddressbook) add_subdirectory(sieveeditor) add_subdirectory(kmailtransport) if(KPimImportWizard_FOUND) add_subdirectory(akonadi-import-wizard) endif() install(FILES kdepim-addons.categories kdepim-addons.renamecategories DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR}) feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/plugins/messageviewer/bodypartformatter/calendar/text_calendar.cpp b/plugins/messageviewer/bodypartformatter/calendar/text_calendar.cpp index 232c1f1a..9ddbecce 100644 --- a/plugins/messageviewer/bodypartformatter/calendar/text_calendar.cpp +++ b/plugins/messageviewer/bodypartformatter/calendar/text_calendar.cpp @@ -1,1513 +1,1511 @@ /* This file is part of kdepim. Copyright (c) 2004 Cornelius Schumacher Copyright (c) 2007 Volker Krause Copyright (c) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company Copyright (C) 2017-2019 Laurent Montel This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA In addition, as a special exception, the copyright holders give permission to link the code of this program with any edition of the Qt library by Trolltech AS, Norway (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. */ #include "attendeeselector.h" #include "calendarinterface.h" #include "delegateselector.h" #include "memorycalendarmemento.h" #include "syncitiphandler.h" #include "reactiontoinvitationdialog.h" #include #include #include #include #include #include #include #include #include using namespace MessageViewer; #include using namespace KCalCore; #include #include #include #include #include #include #include "text_calendar_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace MailTransport; namespace { static bool hasMyWritableEventsFolders(const QString &family) { QString myfamily = family; if (family.isEmpty()) { myfamily = QStringLiteral("calendar"); } #if 0 // TODO port to Akonadi #ifndef KDEPIM_NO_KRESOURCES CalendarResourceManager manager(myfamily); manager.readConfig(); CalendarResourceManager::ActiveIterator it; for (it = manager.activeBegin(); it != manager.activeEnd(); ++it) { if ((*it)->readOnly()) { continue; } const QStringList subResources = (*it)->subresources(); if (subResources.isEmpty()) { return true; } QStringList::ConstIterator subIt; for (subIt = subResources.begin(); subIt != subResources.end(); ++subIt) { if (!(*it)->subresourceActive((*subIt))) { continue; } if ((*it)->type() == "imap" || (*it)->type() == "kolab") { if ((*it)->subresourceType((*subIt)) == "todo" || (*it)->subresourceType((*subIt)) == "journal" || !(*subIt).contains("/.INBOX.directory/")) { continue; } } return true; } } return false; #endif #else qCDebug(TEXT_CALENDAR_LOG) << "Disabled code, port to Akonadi"; return true; #endif } static bool occurredAlready(const Incidence::Ptr &incidence) { Q_ASSERT(incidence); const QDateTime now = QDateTime::currentDateTime(); const QDate today = now.date(); if (incidence->recurs()) { const QDateTime nextDate = incidence->recurrence()->getNextDateTime(now); return !nextDate.isValid(); } else { const QDateTime incidenceDate = incidence->dateTime(Incidence::RoleDisplayEnd); if (incidenceDate.isValid()) { return incidence->allDay() ? (incidenceDate.date() < today) : (incidenceDate < QDateTime::currentDateTime()); } } return false; } class KMInvitationFormatterHelper : public KCalUtils::InvitationFormatterHelper { public: KMInvitationFormatterHelper(const MimeTreeParser::MessagePartPtr &bodyPart, const KCalCore::MemoryCalendar::Ptr &calendar) : mBodyPart(bodyPart) , mCalendar(calendar) { } QString generateLinkURL(const QString &id) override { return mBodyPart->makeLink(id); } KCalCore::Calendar::Ptr calendar() const override { return mCalendar; } private: MimeTreeParser::MessagePartPtr mBodyPart; KCalCore::MemoryCalendar::Ptr mCalendar; }; class Formatter : public MessageViewer::MessagePartRendererBase { public: bool render(const MimeTreeParser::MessagePartPtr &msgPart, MessageViewer::HtmlWriter *writer, MessageViewer::RenderContext *) const override { QMimeDatabase db; auto mt = db.mimeTypeForName(QString::fromLatin1(msgPart->content()->contentType()->mimeType().toLower())); if (!mt.isValid() || mt.name() != QLatin1String("text/calendar")) { return false; } auto nodeHelper = msgPart->nodeHelper(); if (!nodeHelper) { return false; } /** Formatting is async now because we need to fetch incidences from akonadi. Basically this method (format()) will be called twice. The first time it creates the memento that fetches incidences and returns. When the memento finishes, this is called a second time, and we can proceed. BodyPartMementos are documented in MessageViewer/ObjectTreeParser */ MemoryCalendarMemento *memento = dynamic_cast(msgPart->memento()); if (memento) { KMime::Message *const message = dynamic_cast(msgPart->content()->topLevel()); if (!message) { qCWarning(TEXT_CALENDAR_LOG) << "The top-level content is not a message. Cannot handle the invitation then."; return false; } if (memento->finished()) { KMInvitationFormatterHelper helper(msgPart, memento->calendar()); QString source; // If the bodypart does not have a charset specified, we need to fall back to utf8, // not the KMail fallback encoding, so get the contents as binary and decode explicitly. if (msgPart->content()->contentType()->parameter(QStringLiteral("charset")).isEmpty()) { const QByteArray &ba = msgPart->content()->decodedContent(); source = QString::fromUtf8(ba); } else { source = msgPart->text(); } MemoryCalendar::Ptr cl(new MemoryCalendar(QTimeZone::systemTimeZone())); const QString html = KCalUtils::IncidenceFormatter::formatICalInvitationNoHtml( source, cl, &helper, message->sender()->asUnicodeString()); if (html.isEmpty()) { return false; } writer->write(html); } } else { MemoryCalendarMemento *memento = new MemoryCalendarMemento(); msgPart->setMemento(memento); QObject::connect(memento, &MemoryCalendarMemento::update, nodeHelper, &MimeTreeParser::NodeHelper::update); } return true; } }; static QString directoryForStatus(Attendee::PartStat status) { QString dir; switch (status) { case Attendee::Accepted: dir = QStringLiteral("accepted"); break; case Attendee::Tentative: dir = QStringLiteral("tentative"); break; case Attendee::Declined: dir = QStringLiteral("cancel"); break; case Attendee::Delegated: dir = QStringLiteral("delegated"); break; case Attendee::NeedsAction: dir = QStringLiteral("request"); break; default: break; } return dir; } static Incidence::Ptr stringToIncidence(const QString &iCal) { MemoryCalendar::Ptr calendar(new MemoryCalendar(QTimeZone::systemTimeZone())); ICalFormat format; ScheduleMessage::Ptr message = format.parseScheduleMessage(calendar, iCal); if (!message) { //TODO: Error message? qCWarning(TEXT_CALENDAR_LOG) << "Can't parse this ical string: " << iCal; return Incidence::Ptr(); } return message->event().dynamicCast(); } class UrlHandler : public MessageViewer::Interface::BodyPartURLHandler { public: UrlHandler() { //qCDebug(TEXT_CALENDAR_LOG) << "UrlHandler() (iCalendar)"; } QString name() const override { return QStringLiteral("calendar handler"); } Attendee findMyself(const Incidence::Ptr &incidence, const QString &receiver) const { const Attendee::List attendees = incidence->attendees(); const auto idx = findMyself(attendees, receiver); if (idx >= 0) { return attendees.at(idx); } return {}; } int findMyself(const Attendee::List &attendees, const QString &receiver) const { // Find myself. There will always be all attendees listed, even if // only I need to answer it. for (int i = 0; i < attendees.size(); ++i) { // match only the email part, not the name if (KEmailAddress::compareEmail(attendees.at(i).email(), receiver, false)) { // We are the current one, and even the receiver, note // this and quit searching. return i; break; } } return -1; } static bool heuristicalRSVP(const Incidence::Ptr &incidence) { bool rsvp = true; // better send superfluously than not at all const Attendee::List attendees = incidence->attendees(); Attendee::List::ConstIterator it; Attendee::List::ConstIterator end(attendees.constEnd()); for (it = attendees.constBegin(); it != end; ++it) { if (it == attendees.constBegin()) { rsvp = (*it).RSVP(); // use what the first one has } else { if ((*it).RSVP() != rsvp) { rsvp = true; // they differ, default break; } } } return rsvp; } static Attendee::Role heuristicalRole(const Incidence::Ptr &incidence) { Attendee::Role role = Attendee::OptParticipant; const Attendee::List attendees = incidence->attendees(); Attendee::List::ConstIterator it; Attendee::List::ConstIterator end = attendees.constEnd(); for (it = attendees.constBegin(); it != end; ++it) { if (it == attendees.constBegin()) { role = (*it).role(); // use what the first one has } else { if ((*it).role() != role) { role = Attendee::OptParticipant; // they differ, default break; } } } return role; } - static Attachment::Ptr findAttachment(const QString &name, const QString &iCal) + static Attachment findAttachment(const QString &name, const QString &iCal) { Incidence::Ptr incidence = stringToIncidence(iCal); // get the attachment by name from the incidence Attachment::List attachments = incidence->attachments(); - Attachment::Ptr attachment; - if (!attachments.isEmpty()) { - const Attachment::List::ConstIterator end = attachments.constEnd(); - for (Attachment::List::ConstIterator it = attachments.constBegin(); it != end; ++it) { - if ((*it)->label() == name) { - attachment = *it; - break; - } + Attachment attachment; + const Attachment::List::ConstIterator end = attachments.constEnd(); + for (Attachment::List::ConstIterator it = attachments.constBegin(); it != end; ++it) { + if ((*it).label() == name) { + attachment = *it; + break; } } - if (!attachment) { + if (attachment.isEmpty()) { KMessageBox::error( nullptr, i18n("No attachment named \"%1\" found in the invitation.", name)); - return Attachment::Ptr(); + return Attachment(); } - if (attachment->isUri()) { + if (attachment.isUri()) { bool fileExists = false; - QUrl attachmentUrl(attachment->uri()); + QUrl attachmentUrl(attachment.uri()); if (attachmentUrl.isLocalFile()) { fileExists = QFile::exists(attachmentUrl.toLocalFile()); } else { auto job = KIO::stat(attachmentUrl, KIO::StatJob::SourceSide, 0); fileExists = job->exec(); } if (!fileExists) { KMessageBox::information( nullptr, i18n("The invitation attachment \"%1\" is a web link that " "is inaccessible from this computer. Please ask the event " "organizer to resend the invitation with this attachment " "stored inline instead of a link.", attachmentUrl.toDisplayString())); - return Attachment::Ptr(); + return Attachment(); } } return attachment; } static QString findReceiver(KMime::Content *node) { if (!node || !node->topLevel()) { return QString(); } QString receiver; KIdentityManagement::IdentityManager *im = KIdentityManagement::IdentityManager::self(); KMime::Types::Mailbox::List addrs; if (auto header = node->topLevel()->header()) { addrs = header->mailboxes(); } int found = 0; QVector< KMime::Types::Mailbox >::const_iterator end = addrs.constEnd(); for (QVector< KMime::Types::Mailbox >::const_iterator it = addrs.constBegin(); it != end; ++it) { if (im->identityForAddress(QLatin1String((*it).address())) != KIdentityManagement::Identity::null()) { // Ok, this could be us ++found; receiver = QLatin1String((*it).address()); } } KMime::Types::Mailbox::List ccaddrs; if (auto header = node->topLevel()->header()) { ccaddrs = header->mailboxes(); } end = ccaddrs.constEnd(); for (QVector::const_iterator it = ccaddrs.constBegin(); it != end; ++it) { if (im->identityForAddress(QLatin1String((*it).address())) != KIdentityManagement::Identity::null()) { // Ok, this could be us ++found; receiver = QLatin1String((*it).address()); } } if (found != 1) { QStringList possibleAddrs; bool ok; QString selectMessage; if (found == 0) { selectMessage = i18n("None of your identities match the receiver of this message,
" "please choose which of the following addresses is yours,
if any, " "or select one of your identities to use in the reply:
"); possibleAddrs += im->allEmails(); } else { selectMessage = i18n("Several of your identities match the receiver of this message,
" "please choose which of the following addresses is yours:
"); for (const KMime::Types::Mailbox &mbx : qAsConst(addrs)) { possibleAddrs.append(QLatin1String(mbx.address())); } for (const KMime::Types::Mailbox &mbx : qAsConst(ccaddrs)) { possibleAddrs.append(QLatin1String(mbx.address())); } } // select default identity by default const QString defaultAddr = im->defaultIdentity().primaryEmailAddress(); const int defaultIndex = qMax(0, possibleAddrs.indexOf(defaultAddr)); receiver = QInputDialog::getItem(nullptr, i18n("Select Address"), selectMessage, possibleAddrs, defaultIndex, false, &ok); if (!ok) { receiver.clear(); } } return receiver; } Attendee setStatusOnMyself(const Incidence::Ptr &incidence, const Attendee &myself, Attendee::PartStat status, const QString &receiver) const { QString name; QString email; KEmailAddress::extractEmailAddressAndName(receiver, email, name); if (name.isEmpty() && !myself.isNull()) { name = myself.name(); } if (email.isEmpty() && !myself.isNull()) { email = myself.email(); } Q_ASSERT(!email.isEmpty()); // delivery must be possible Attendee newMyself(name, email, true, // RSVP, otherwise we would not be here status, !myself.isNull() ? myself.role() : heuristicalRole(incidence), myself.uid()); if (!myself.isNull()) { newMyself.setDelegate(myself.delegate()); newMyself.setDelegator(myself.delegator()); } // Make sure only ourselves is in the event incidence->clearAttendees(); if (!newMyself.isNull()) { incidence->addAttendee(newMyself); } return newMyself; } enum MailType { Answer, Delegation, Forward, DeclineCounter }; bool mailICal(const QString &receiver, const QString &to, const QString &iCal, const QString &subject, const QString &status, bool delMessage, Viewer *viewerInstance) const { qCDebug(TEXT_CALENDAR_LOG) << "Mailing message:" << iCal; KMime::Message::Ptr msg(new KMime::Message); if (MessageViewer::MessageViewerSettings::self()->exchangeCompatibleInvitations()) { msg->subject()->fromUnicodeString(status, "utf-8"); QString tsubject = subject; tsubject.remove(i18n("Answer: ")); if (status == QLatin1String("cancel")) { msg->subject()->fromUnicodeString( i18nc("Not able to attend.", "Declined: %1", tsubject), "utf-8"); } else if (status == QLatin1String("tentative")) { msg->subject()->fromUnicodeString( i18nc("Unsure if it is possible to attend.", "Tentative: %1", tsubject), "utf-8"); } else if (status == QLatin1String("accepted")) { msg->subject()->fromUnicodeString( i18nc("Accepted the invitation.", "Accepted: %1", tsubject), "utf-8"); } else { msg->subject()->fromUnicodeString(subject, "utf-8"); } } else { msg->subject()->fromUnicodeString(subject, "utf-8"); } msg->to()->fromUnicodeString(to, "utf-8"); msg->from()->fromUnicodeString(receiver, "utf-8"); msg->date()->setDateTime(QDateTime::currentDateTime()); if (MessageViewer::MessageViewerSettings::self()->legacyBodyInvites()) { msg->contentType()->setMimeType("text/calendar"); msg->contentType()->setCharset("utf-8"); msg->contentType()->setName(QStringLiteral("cal.ics"), "utf-8"); msg->contentType()->setParameter(QStringLiteral("method"), QStringLiteral("reply")); KMime::Headers::ContentDisposition *disposition = new KMime::Headers::ContentDisposition; disposition->setDisposition(KMime::Headers::CDinline); msg->setHeader(disposition); msg->contentTransferEncoding()->setEncoding(KMime::Headers::CEquPr); const QString answer = i18n("Invitation answer attached."); msg->setBody(answer.toUtf8()); } else { // We need to set following 4 lines by hand else KMime::Content::addContent // will create a new Content instance for us to attach the main message // what we don't need cause we already have the main message instance where // 2 additional messages are attached. KMime::Headers::ContentType *ct = msg->contentType(); ct->setMimeType("multipart/mixed"); ct->setBoundary(KMime::multiPartBoundary()); ct->setCategory(KMime::Headers::CCcontainer); // Set the first multipart, the body message. KMime::Content *bodyMessage = new KMime::Content; KMime::Headers::ContentDisposition *bodyDisposition = new KMime::Headers::ContentDisposition; bodyDisposition->setDisposition(KMime::Headers::CDinline); bodyMessage->contentType()->setMimeType("text/plain"); bodyMessage->contentType()->setCharset("utf-8"); bodyMessage->contentTransferEncoding()->setEncoding(KMime::Headers::CEquPr); const QString answer = i18n("Invitation answer attached."); bodyMessage->setBody(answer.toUtf8()); bodyMessage->setHeader(bodyDisposition); msg->addContent(bodyMessage); // Set the second multipart, the attachment. KMime::Content *attachMessage = new KMime::Content; KMime::Headers::ContentDisposition *attachDisposition = new KMime::Headers::ContentDisposition; attachDisposition->setDisposition(KMime::Headers::CDattachment); attachMessage->contentType()->setMimeType("text/calendar"); attachMessage->contentType()->setCharset("utf-8"); attachMessage->contentType()->setName(QStringLiteral("cal.ics"), "utf-8"); attachMessage->contentType()->setParameter(QStringLiteral("method"), QStringLiteral("reply")); attachMessage->setHeader(attachDisposition); attachMessage->contentTransferEncoding()->setEncoding(KMime::Headers::CEquPr); attachMessage->setBody(KMime::CRLFtoLF(iCal.toUtf8())); msg->addContent(attachMessage); } // Try and match the receiver with an identity. // Setting the identity here is important, as that is used to select the correct // transport later KIdentityManagement::IdentityManager *im = KIdentityManagement::IdentityManager::self(); const KIdentityManagement::Identity identity = im->identityForAddress( findReceiver(viewerInstance->message().data())); const bool nullIdentity = (identity == KIdentityManagement::Identity::null()); if (!nullIdentity) { KMime::Headers::Generic *x_header = new KMime::Headers::Generic("X-KMail-Identity"); x_header->from7BitString(QByteArray::number(identity.uoid())); msg->setHeader(x_header); } const bool identityHasTransport = !identity.transport().isEmpty(); int transportId = -1; if (!nullIdentity && identityHasTransport) { transportId = identity.transport().toInt(); } else { transportId = TransportManager::self()->defaultTransportId(); } if (transportId == -1) { if (!TransportManager::self()->showTransportCreationDialog(nullptr, TransportManager::IfNoTransportExists)) { return false; } transportId = TransportManager::self()->defaultTransportId(); } auto header = new KMime::Headers::Generic("X-KMail-Transport"); header->fromUnicodeString(QString::number(transportId), "utf-8"); msg->setHeader(header); // Outlook will only understand the reply if the From: header is the // same as the To: header of the invitation message. if (!MessageViewer::MessageViewerSettings::self()->legacyMangleFromToHeaders()) { if (identity != KIdentityManagement::Identity::null()) { msg->from()->fromUnicodeString(identity.fullEmailAddr(), "utf-8"); } // Remove BCC from identity on ical invitations (kolab/issue474) msg->removeHeader(); } msg->assemble(); MailTransport::Transport *transport = MailTransport::TransportManager::self()->transportById(transportId); MailTransport::MessageQueueJob *job = new MailTransport::MessageQueueJob; job->addressAttribute().setTo(QStringList() << KEmailAddress::extractEmailAddress( KEmailAddress::normalizeAddressesAndEncodeIdn(to))); job->transportAttribute().setTransportId(transport->id()); if (transport->specifySenderOverwriteAddress()) { job->addressAttribute().setFrom( KEmailAddress::extractEmailAddress( KEmailAddress::normalizeAddressesAndEncodeIdn(transport->senderOverwriteAddress()))); } else { job->addressAttribute().setFrom( KEmailAddress::extractEmailAddress( KEmailAddress::normalizeAddressesAndEncodeIdn(msg->from()->asUnicodeString()))); } job->setMessage(msg); if (!job->exec()) { qCWarning(TEXT_CALENDAR_LOG) << "Error queuing message in outbox:" << job->errorText(); return false; } // We are not notified when mail was sent, so assume it was sent when queued. if (delMessage && MessageViewer::MessageViewerSettings::self()->deleteInvitationEmailsAfterSendingReply()) { viewerInstance->deleteMessage(); } return true; } bool mail(Viewer *viewerInstance, const Incidence::Ptr &incidence, const QString &status, iTIPMethod method = iTIPReply, const QString &receiver = QString(), const QString &to = QString(), MailType type = Answer) const { //status is accepted/tentative/declined ICalFormat format; format.setTimeZone(QTimeZone::systemTimeZone()); QString msg = format.createScheduleMessage(incidence, method); QString summary = incidence->summary(); if (summary.isEmpty()) { summary = i18n("Incidence with no summary"); } QString subject; switch (type) { case Answer: subject = i18n("Answer: %1", summary); break; case Delegation: subject = i18n("Delegated: %1", summary); break; case Forward: subject = i18n("Forwarded: %1", summary); break; case DeclineCounter: subject = i18n("Declined Counter Proposal: %1", summary); break; } // Set the organizer to the sender, if the ORGANIZER hasn't been set. if (incidence->organizer().isEmpty()) { QString tname, temail; KMime::Message::Ptr message = viewerInstance->message(); KEmailAddress::extractEmailAddressAndName(message->sender()->asUnicodeString(), temail, tname); incidence->setOrganizer(Person(tname, temail)); } QString recv = to; if (recv.isEmpty()) { recv = incidence->organizer().fullName(); } return mailICal(receiver, recv, msg, subject, status, type != Forward, viewerInstance); } bool saveFile(const QString &receiver, const QString &iCal, const QString &type, MimeTreeParser::Interface::BodyPart *bodyPart) const { MemoryCalendarMemento *memento = dynamic_cast(bodyPart->memento()); // This will block. There's no way to make it async without refactoring the memento mechanism SyncItipHandler *itipHandler = new SyncItipHandler(receiver, iCal, type, memento->calendar()); // If result is ResultCancelled, then we don't show the message box and return false so kmail // doesn't delete the e-mail. qCDebug(TEXT_CALENDAR_LOG) << "ITIPHandler result was " << itipHandler->result(); const Akonadi::ITIPHandler::Result res = itipHandler->result(); if (res == Akonadi::ITIPHandler::ResultError) { const QString errorMessage = itipHandler->errorMessage(); if (!errorMessage.isEmpty()) { qCCritical(TEXT_CALENDAR_LOG) << "Error while processing invitation: " << errorMessage; KMessageBox::error(nullptr, errorMessage); } return false; } return res; } bool cancelPastInvites(const Incidence::Ptr incidence, const QString &path) const { QString warnStr; QDateTime now = QDateTime::currentDateTime(); QDate today = now.date(); Incidence::IncidenceType type = Incidence::TypeUnknown; const bool occurred = occurredAlready(incidence); if (incidence->type() == Incidence::TypeEvent) { type = Incidence::TypeEvent; Event::Ptr event = incidence.staticCast(); if (!event->allDay()) { if (occurred) { warnStr = i18n("\"%1\" occurred already.", event->summary()); } else if (event->dtStart() <= now && now <= event->dtEnd()) { warnStr = i18n("\"%1\" is currently in-progress.", event->summary()); } } else { if (occurred) { warnStr = i18n("\"%1\" occurred already.", event->summary()); } else if (event->dtStart().date() <= today && today <= event->dtEnd().date()) { warnStr = i18n("\"%1\", happening all day today, is currently in-progress.", event->summary()); } } } else if (incidence->type() == Incidence::TypeTodo) { type = Incidence::TypeTodo; Todo::Ptr todo = incidence.staticCast(); if (!todo->allDay()) { if (todo->hasDueDate()) { if (todo->dtDue() < now) { warnStr = i18n("\"%1\" is past due.", todo->summary()); } else if (todo->hasStartDate() && todo->dtStart() <= now && now <= todo->dtDue()) { warnStr = i18n("\"%1\" is currently in-progress.", todo->summary()); } } else if (todo->hasStartDate()) { if (todo->dtStart() < now) { warnStr = i18n("\"%1\" has already started.", todo->summary()); } } } else { if (todo->hasDueDate()) { if (todo->dtDue().date() < today) { warnStr = i18n("\"%1\" is past due.", todo->summary()); } else if (todo->hasStartDate() && todo->dtStart().date() <= today && today <= todo->dtDue().date()) { warnStr = i18n("\"%1\", happening all-day today, is currently in-progress.", todo->summary()); } } else if (todo->hasStartDate()) { if (todo->dtStart().date() < today) { warnStr = i18n("\"%1\", happening all day, has already started.", todo->summary()); } } } } if (!warnStr.isEmpty()) { QString queryStr; if (path == QLatin1String("accept")) { if (type == Incidence::TypeTodo) { queryStr = i18n("Do you still want to accept the task?"); } else { queryStr = i18n("Do you still want to accept the invitation?"); } } else if (path == QLatin1String("accept_conditionally")) { if (type == Incidence::TypeTodo) { queryStr = i18n("Do you still want to send conditional acceptance of the invitation?"); } else { queryStr = i18n("Do you still want to send conditional acceptance of the task?"); } } else if (path == QLatin1String("accept_counter")) { queryStr = i18n("Do you still want to accept the counter proposal?"); } else if (path == QLatin1String("counter")) { queryStr = i18n("Do you still want to send a counter proposal?"); } else if (path == QLatin1String("decline")) { queryStr = i18n("Do you still want to send a decline response?"); } else if (path == QLatin1String("decline_counter")) { queryStr = i18n("Do you still want to decline the counter proposal?"); } else if (path == QLatin1String("reply")) { queryStr = i18n("Do you still want to record this response in your calendar?"); } else if (path == QLatin1String("delegate")) { if (type == Incidence::TypeTodo) { queryStr = i18n("Do you still want to delegate this task?"); } else { queryStr = i18n("Do you still want to delegate this invitation?"); } } else if (path == QLatin1String("forward")) { if (type == Incidence::TypeTodo) { queryStr = i18n("Do you still want to forward this task?"); } else { queryStr = i18n("Do you still want to forward this invitation?"); } } else if (path == QLatin1String("cancel")) { if (type == Incidence::TypeTodo) { queryStr = i18n("Do you still want to cancel this task?"); } else { queryStr = i18n("Do you still want to cancel this invitation?"); } } else if (path == QLatin1String("check_calendar")) { queryStr = i18n("Do you still want to check your calendar?"); } else if (path == QLatin1String("record")) { if (type == Incidence::TypeTodo) { queryStr = i18n("Do you still want to record this task in your calendar?"); } else { queryStr = i18n("Do you still want to record this invitation in your calendar?"); } } else if (path == QLatin1String("cancel")) { if (type == Incidence::TypeTodo) { queryStr = i18n("Do you really want to cancel this task?"); } else { queryStr = i18n("Do you really want to cancel this invitation?"); } } else if (path.startsWith(QLatin1String("ATTACH:"))) { return false; } else { queryStr = i18n("%1?", path); } if (KMessageBox::warningYesNo( nullptr, i18n("%1\n%2", warnStr, queryStr)) == KMessageBox::No) { return true; } } return false; } bool handleInvitation(const QString &iCal, Attendee::PartStat status, MimeTreeParser::Interface::BodyPart *part, Viewer *viewerInstance) const { bool ok = true; const QString receiver = findReceiver(part->content()); qCDebug(TEXT_CALENDAR_LOG) << receiver; if (receiver.isEmpty()) { // Must be some error. Still return true though, since we did handle it return true; } Incidence::Ptr incidence = stringToIncidence(iCal); qCDebug(TEXT_CALENDAR_LOG) << "Handling invitation: uid is : " << incidence->uid() << "; schedulingId is:" << incidence->schedulingID() << "; Attendee::PartStat = " << status; // get comment for tentative acceptance if (askForComment(status)) { QPointer dlg = new ReactionToInvitationDialog(nullptr); dlg->setWindowTitle(i18n("Reaction to Invitation")); QString comment; if (dlg->exec()) { comment = dlg->comment(); delete dlg; } else { delete dlg; return true; } if (comment.trimmed().isEmpty()) { KMessageBox::error( nullptr, i18n("You forgot to add proposal. Please add it. Thanks")); return true; } else { incidence->addComment(comment); } } // First, save it for KOrganizer to handle const QString dir = directoryForStatus(status); if (dir.isEmpty()) { qCWarning(TEXT_CALENDAR_LOG) << "Impossible to understand status: " << status; return true; // unknown status } if (status != Attendee::Delegated) { // we do that below for delegated incidences if (!saveFile(receiver, iCal, dir, part)) { return false; } } QString delegateString; bool delegatorRSVP = false; if (status == Attendee::Delegated) { DelegateSelector dlg; if (dlg.exec() == QDialog::Rejected) { return true; } delegateString = dlg.delegate(); delegatorRSVP = dlg.rsvp(); if (delegateString.isEmpty()) { return true; } if (KEmailAddress::compareEmail(delegateString, incidence->organizer().email(), false)) { KMessageBox::sorry(nullptr, i18n("Delegation to organizer is not possible.")); return true; } } if (!incidence) { return false; } const Attendee myself = findMyself(incidence, receiver); // find our delegator, we need to inform him as well QString delegator; if (status != Attendee::NeedsAction && !myself.isNull() && !myself.delegator().isEmpty()) { const Attendee::List attendees = incidence->attendees(); Attendee::List::ConstIterator end = attendees.constEnd(); for (Attendee::List::ConstIterator it = attendees.constBegin(); it != end; ++it) { if (KEmailAddress::compareEmail((*it).fullName(), myself.delegator(), false) && (*it).status() == Attendee::Delegated) { delegator = (*it).fullName(); delegatorRSVP = (*it).RSVP(); break; } } } if (status != Attendee::NeedsAction && ((!myself.isNull() && (myself.RSVP() || myself.status() == Attendee::NeedsAction)) || heuristicalRSVP(incidence))) { Attendee newMyself = setStatusOnMyself(incidence, myself, status, receiver); if (!newMyself.isNull() && status == Attendee::Delegated) { newMyself.setDelegate(delegateString); newMyself.setRSVP(delegatorRSVP); } ok = mail(viewerInstance, incidence, dir, iTIPReply, receiver); // check if we need to inform our delegator about this as well if (!newMyself.isNull() && (status == Attendee::Accepted || status == Attendee::Declined) && !delegator.isEmpty()) { if (delegatorRSVP || status == Attendee::Declined) { ok = mail(viewerInstance, incidence, dir, iTIPReply, receiver, delegator); } } } else if (myself.isNull() && (status != Attendee::Declined && status != Attendee::NeedsAction)) { // forwarded invitation QString name; QString email; KEmailAddress::extractEmailAddressAndName(receiver, email, name); if (!email.isEmpty()) { Attendee newMyself(name, email, true, // RSVP, otherwise we would not be here status, heuristicalRole(incidence), QString()); incidence->clearAttendees(); incidence->addAttendee(newMyself); ok = mail(viewerInstance, incidence, dir, iTIPReply, receiver); } } else { if (MessageViewer::MessageViewerSettings::self()->deleteInvitationEmailsAfterSendingReply()) { viewerInstance->deleteMessage(); } } // create invitation for the delegate (same as the original invitation // with the delegate as additional attendee), we also use that for updating // our calendar if (status == Attendee::Delegated) { incidence = stringToIncidence(iCal); auto attendees = incidence->attendees(); const int myselfIdx = findMyself(attendees, receiver); if (myselfIdx >= 0) { attendees[myselfIdx].setStatus(status); attendees[myselfIdx].setDelegate(delegateString); incidence->setAttendees(attendees); } QString name, email; KEmailAddress::extractEmailAddressAndName(delegateString, email, name); Attendee delegate(name, email, true); delegate.setDelegator(receiver); incidence->addAttendee(delegate); ICalFormat format; format.setTimeZone(QTimeZone::systemTimeZone()); const QString iCal = format.createScheduleMessage(incidence, iTIPRequest); if (!saveFile(receiver, iCal, dir, part)) { return false; } ok = mail(viewerInstance, incidence, dir, iTIPRequest, receiver, delegateString, Delegation); } return ok; } bool openAttachment(const QString &name, const QString &iCal) const { - Attachment::Ptr attachment(findAttachment(name, iCal)); - if (!attachment) { + Attachment attachment(findAttachment(name, iCal)); + if (attachment.isEmpty()) { return false; } - if (attachment->isUri()) { - QDesktopServices::openUrl(QUrl(attachment->uri())); + if (attachment.isUri()) { + QDesktopServices::openUrl(QUrl(attachment.uri())); } else { // put the attachment in a temporary file and launch it QTemporaryFile *file = nullptr; QMimeDatabase db; - QStringList patterns = db.mimeTypeForName(attachment->mimeType()).globPatterns(); + QStringList patterns = db.mimeTypeForName(attachment.mimeType()).globPatterns(); if (!patterns.empty()) { QString pattern = patterns.at(0); file = new QTemporaryFile(QDir::tempPath() + QStringLiteral("/messageviewer_XXXXXX") + pattern.remove(QLatin1Char('*'))); } else { file = new QTemporaryFile(); } file->setAutoRemove(false); file->open(); file->setPermissions(QFile::ReadUser); - file->write(QByteArray::fromBase64(attachment->data())); + file->write(QByteArray::fromBase64(attachment.data())); file->close(); KRun::RunFlags flags; flags |= KRun::DeleteTemporaryFiles; - bool stat = KRun::runUrl(QUrl::fromLocalFile(file->fileName()), attachment->mimeType(), nullptr, flags); + bool stat = KRun::runUrl(QUrl::fromLocalFile(file->fileName()), attachment.mimeType(), nullptr, flags); delete file; return stat; } return true; } bool saveAsAttachment(const QString &name, const QString &iCal) const { - Attachment::Ptr a(findAttachment(name, iCal)); - if (!a) { + Attachment a(findAttachment(name, iCal)); + if (a.isEmpty()) { return false; } // get the saveas file name const QString saveAsFile = QFileDialog::getSaveFileName(nullptr, i18n("Save Invitation Attachment"), name, QString()); if (saveAsFile.isEmpty()) { return false; } bool stat = false; - if (a->isUri()) { + if (a.isUri()) { // save the attachment url - auto job = KIO::file_copy(QUrl(a->uri()), QUrl::fromLocalFile(saveAsFile)); + auto job = KIO::file_copy(QUrl(a.uri()), QUrl::fromLocalFile(saveAsFile)); stat = job->exec(); } else { // put the attachment in a temporary file and save it QTemporaryFile *file{ nullptr }; QMimeDatabase db; - QStringList patterns = db.mimeTypeForName(a->mimeType()).globPatterns(); + QStringList patterns = db.mimeTypeForName(a.mimeType()).globPatterns(); if (!patterns.empty()) { QString pattern = patterns.at(0); file = new QTemporaryFile(QDir::tempPath() + QStringLiteral("/messageviewer_XXXXXX") + pattern.remove(QLatin1Char('*'))); } else { file = new QTemporaryFile(); } file->setAutoRemove(false); file->open(); file->setPermissions(QFile::ReadUser); - file->write(QByteArray::fromBase64(a->data())); + file->write(QByteArray::fromBase64(a.data())); file->close(); const QString filename = file->fileName(); delete file; auto job = KIO::file_copy(QUrl::fromLocalFile(filename), QUrl::fromLocalFile(saveAsFile)); stat = job->exec(); } return stat; } bool ensureKorganizerRunning(bool switchTo) const { // FIXME: this function should be inside a QObject, and async, // and Q_EMIT a signal when korg registered itself successfully // Or better, use DBus activation in all cases. QString error; bool result = true; QString dbusService; #if defined(Q_OS_WIN32) //Can't run the korganizer-mobile.sh through KDBusServiceStarter in these platforms. QDBusInterface *interface = new QDBusInterface(QLatin1String("org.kde.korganizer"), QStringLiteral("/MainApplication")); if (!interface->isValid()) { qCDebug(TEXT_CALENDAR_LOG) << "Starting korganizer..."; QDBusServiceWatcher *watcher = new QDBusServiceWatcher(QLatin1String("org.kde.korganizer"), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForRegistration); QEventLoop loop; watcher->connect(watcher, &QDBusServiceWatcher::serviceRegistered, &loop, &QEventLoop::quit); result = QProcess::startDetached(QLatin1String("korganizer")); if (result) { qCDebug(TEXT_CALENDAR_LOG) << "Starting loop"; loop.exec(); qCDebug(TEXT_CALENDAR_LOG) << "Korganizer finished starting"; } else { qCWarning(TEXT_CALENDAR_LOG) << "Failed to start korganizer with QProcess"; } delete watcher; } delete interface; #else QString constraint; result = KDBusServiceStarter::self()->findServiceFor(QStringLiteral("DBUS/Organizer"), constraint, &error, &dbusService) == 0; #endif if (result) { // OK, so korganizer (or kontact) is running. Now ensure the object we want is loaded. QDBusInterface iface(QStringLiteral("org.kde.korganizer"), QStringLiteral("/MainApplication"), QStringLiteral("org.kde.PIMUniqueApplication")); if (iface.isValid()) { if (switchTo) { iface.call(QStringLiteral("newInstance")); // activate korganizer window } #if 0 //Not exist QDBusInterface pimIface("org.kde.korganizer", "/korganizer_PimApplication", "org.kde.PIMUniqueApplication"); QDBusReply r = pimIface.call("load"); if (!r.isValid() || !r.value()) { qCWarning(TEXT_CALENDAR_LOG) << "Loading korganizer failed: " << pimIface.lastError().message(); } #endif } else { qCWarning(TEXT_CALENDAR_LOG) << "Couldn't obtain korganizer D-Bus interface" << iface.lastError().message(); } // We don't do anything with it, we just need it to be running so that it handles // the incoming directory. } else { qCWarning(TEXT_CALENDAR_LOG) << "Couldn't start DBUS/Organizer:" << dbusService << error; } return result; } void showCalendar(const QDate &date) const { if (ensureKorganizerRunning(true)) { QDBusInterface *kontact = new QDBusInterface(QStringLiteral("org.kde.kontact"), QStringLiteral("/KontactInterface"), QStringLiteral("org.kde.kontact.KontactInterface"), QDBusConnection::sessionBus()); if (kontact->isValid()) { kontact->call(QStringLiteral("selectPlugin"), QStringLiteral("kontact_korganizerplugin")); } delete kontact; OrgKdeKorganizerCalendarInterface *iface = new OrgKdeKorganizerCalendarInterface(QStringLiteral("org.kde.korganizer"), QStringLiteral("/Calendar"), QDBusConnection::sessionBus(), nullptr); if (!iface->isValid()) { qCDebug(TEXT_CALENDAR_LOG) << "Calendar interface is not valid! " << iface->lastError().message(); delete iface; return; } iface->showEventView(); iface->showDate(date); delete iface; } } bool handleIgnore(Viewer *viewerInstance) const { // simply move the message to trash viewerInstance->deleteMessage(); return true; } bool handleDeclineCounter(const QString &iCal, MimeTreeParser::Interface::BodyPart *part, Viewer *viewerInstance) const { const QString receiver(findReceiver(part->content())); if (receiver.isEmpty()) { return true; } Incidence::Ptr incidence(stringToIncidence(iCal)); if (askForComment(Attendee::Declined)) { QPointer dlg = new ReactionToInvitationDialog(nullptr); dlg->setWindowTitle(i18n("Decline Counter Proposal")); QString comment; if (dlg->exec()) { comment = dlg->comment(); delete dlg; } else { delete dlg; return true; } if (comment.trimmed().isEmpty()) { KMessageBox::error( nullptr, i18n("You forgot to add proposal. Please add it. Thanks")); return true; } else { incidence->addComment(comment); } } return mail(viewerInstance, incidence, QStringLiteral("declinecounter"), KCalCore::iTIPDeclineCounter, receiver, QString(), DeclineCounter); } bool counterProposal(const QString &iCal, MimeTreeParser::Interface::BodyPart *part) const { const QString receiver = findReceiver(part->content()); if (receiver.isEmpty()) { return true; } // Don't delete the invitation here in any case, if the counter proposal // is declined you might need it again. return saveFile(receiver, iCal, QStringLiteral("counter"), part); } bool handleClick(Viewer *viewerInstance, MimeTreeParser::Interface::BodyPart *part, const QString &path) const override { // filter out known paths that don't belong to this type of urlmanager. // kolab/issue4054 msg27201 if (path.contains(QLatin1String("addToAddressBook:")) || path.contains(QLatin1String("updateToAddressBook"))) { return false; } if (!hasMyWritableEventsFolders(QStringLiteral("calendar"))) { KMessageBox::error( nullptr, i18n("You have no writable calendar folders for invitations, " "so storing or saving a response will not be possible.\n" "Please create at least 1 writable events calendar and re-sync.")); return false; } // If the bodypart does not have a charset specified, we need to fall back to utf8, // not the KMail fallback encoding, so get the contents as binary and decode explicitly. QString iCal; if (!part->content()->contentType()->hasParameter(QStringLiteral("charset"))) { const QByteArray &ba = part->content()->decodedContent(); iCal = QString::fromUtf8(ba); } else { iCal = part->content()->decodedText(); } Incidence::Ptr incidence = stringToIncidence(iCal); if (!incidence) { KMessageBox::sorry( nullptr, i18n("The calendar invitation stored in this email message is broken in some way. " "Unable to continue.")); return false; } bool result = false; if (cancelPastInvites(incidence, path)) { return result; } if (path == QLatin1String("accept")) { result = handleInvitation(iCal, Attendee::Accepted, part, viewerInstance); } else if (path == QLatin1String("accept_conditionally")) { result = handleInvitation(iCal, Attendee::Tentative, part, viewerInstance); } else if (path == QLatin1String("counter")) { result = counterProposal(iCal, part); } else if (path == QLatin1String("ignore")) { result = handleIgnore(viewerInstance); } else if (path == QLatin1String("decline")) { result = handleInvitation(iCal, Attendee::Declined, part, viewerInstance); } else if (path == QLatin1String("decline_counter")) { result = handleDeclineCounter(iCal, part, viewerInstance); } else if (path == QLatin1String("postpone")) { result = handleInvitation(iCal, Attendee::NeedsAction, part, viewerInstance); } else if (path == QLatin1String("delegate")) { result = handleInvitation(iCal, Attendee::Delegated, part, viewerInstance); } else if (path == QLatin1String("forward")) { AttendeeSelector dlg; if (dlg.exec() == QDialog::Rejected) { return true; } QString fwdTo = dlg.attendees().join(QStringLiteral(", ")); if (fwdTo.isEmpty()) { return true; } const QString receiver = findReceiver(part->content()); result = mail(viewerInstance, incidence, QStringLiteral("forward"), iTIPRequest, receiver, fwdTo, Forward); } else if (path == QLatin1String("check_calendar")) { incidence = stringToIncidence(iCal); showCalendar(incidence->dtStart().date()); return true; } else if (path == QLatin1String("reply") || path == QLatin1String("cancel") || path == QLatin1String("accept_counter")) { // These should just be saved with their type as the dir const QString p = (path == QLatin1String("accept_counter") ? QStringLiteral("reply") : path); if (saveFile(QStringLiteral("Receiver Not Searched"), iCal, p, part)) { if (MessageViewer::MessageViewerSettings::self()->deleteInvitationEmailsAfterSendingReply()) { viewerInstance->deleteMessage(); } result = true; } } else if (path == QLatin1String("record")) { incidence = stringToIncidence(iCal); QString summary; int response = KMessageBox::questionYesNoCancel( nullptr, i18nc("@info", "The organizer is not expecting a reply to this invitation " "but you can send them an email message if you desire.\n\n" "Would you like to send the organizer a message regarding this invitation?\n" "Press the [Cancel] button to cancel the recording operation."), i18nc("@title:window", "Send Email to Organizer"), KGuiItem(i18n("Do Not Send")), KGuiItem(i18n("Send EMail"))); switch (response) { case KMessageBox::Cancel: break; case KMessageBox::No: { // means "send email" summary = incidence->summary(); if (!summary.isEmpty()) { summary = i18n("Re: %1", summary); } QUrlQuery query; query.addQueryItem(QStringLiteral("to"), incidence->organizer().email()); query.addQueryItem(QStringLiteral("subject"), summary); QUrl url; url.setScheme(QStringLiteral("mailto")); url.setQuery(query); QDesktopServices::openUrl(url); } //fall through case KMessageBox::Yes: // means "do not send" if (saveFile(QStringLiteral("Receiver Not Searched"), iCal, QStringLiteral("reply"), part)) { if (MessageViewer::MessageViewerSettings::self()->deleteInvitationEmailsAfterSendingReply()) { viewerInstance->deleteMessage(); result = true; } } showCalendar(incidence->dtStart().date()); break; } } else if (path == QLatin1String("delete")) { viewerInstance->deleteMessage(); result = true; } if (path.startsWith(QLatin1String("ATTACH:"))) { const QString name = QString::fromUtf8(QByteArray::fromBase64(path.mid(7).toUtf8())); result = openAttachment(name, iCal); } if (result) { // do not close the secondary window if an attachment was opened (kolab/issue4317) if (!path.startsWith(QLatin1String("ATTACH:"))) { qCDebug(TEXT_CALENDAR_LOG) << "AKONADI PORT: Disabled code in " << Q_FUNC_INFO << "about closing if in a secondary window"; #if 0 // TODO port to Akonadi c.closeIfSecondaryWindow(); #endif } } return result; } bool handleContextMenuRequest(MimeTreeParser::Interface::BodyPart *part, const QString &path, const QPoint &point) const override { QString name = path; if (path.startsWith(QLatin1String("ATTACH:"))) { name = QString::fromUtf8(QByteArray::fromBase64(path.mid(7).toUtf8())); } else { return false; //because it isn't an attachment invitation } QString iCal; if (!part->content()->contentType()->hasParameter(QStringLiteral("charset"))) { const QByteArray &ba = part->content()->decodedContent(); iCal = QString::fromUtf8(ba); } else { iCal = part->content()->decodedText(); } QMenu *menu = new QMenu(); QAction *open = menu->addAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Open Attachment")); QAction *saveas = menu->addAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("Save Attachment As...")); QAction *a = menu->exec(point, nullptr); if (a == open) { openAttachment(name, iCal); } else if (a == saveas) { saveAsAttachment(name, iCal); } delete menu; return true; } QString statusBarMessage(MimeTreeParser::Interface::BodyPart *, const QString &path) const override { if (!path.isEmpty()) { if (path == QLatin1String("accept")) { return i18n("Accept invitation"); } else if (path == QLatin1String("accept_conditionally")) { return i18n("Accept invitation conditionally"); } else if (path == QLatin1String("accept_counter")) { return i18n("Accept counter proposal"); } else if (path == QLatin1String("counter")) { return i18n("Create a counter proposal..."); } else if (path == QLatin1String("ignore")) { return i18n("Throw mail away"); } else if (path == QLatin1String("decline")) { return i18n("Decline invitation"); } else if (path == QLatin1String("postpone")) { return i18n("Postpone"); } else if (path == QLatin1String("decline_counter")) { return i18n("Decline counter proposal"); } else if (path == QLatin1String("check_calendar")) { return i18n("Check my calendar..."); } else if (path == QLatin1String("reply")) { return i18n("Record response into my calendar"); } else if (path == QLatin1String("record")) { return i18n("Record invitation into my calendar"); } else if (path == QLatin1String("delete")) { return i18n("Move this invitation to my trash folder"); } else if (path == QLatin1String("delegate")) { return i18n("Delegate invitation"); } else if (path == QLatin1String("forward")) { return i18n("Forward invitation"); } else if (path == QLatin1String("cancel")) { return i18n("Remove invitation from my calendar"); } else if (path.startsWith(QLatin1String("ATTACH:"))) { const QString name = QString::fromUtf8(QByteArray::fromBase64(path.mid(7).toUtf8())); return i18n("Open attachment \"%1\"", name); } } return QString(); } bool askForComment(Attendee::PartStat status) const { if (status != Attendee::NeedsAction && ((status != Attendee::Accepted && MessageViewer::MessageViewerSettings::self()->askForCommentWhenReactingToInvitation() == MessageViewer::MessageViewerSettings::EnumAskForCommentWhenReactingToInvitation::AskForAllButAcceptance) || (MessageViewer::MessageViewerSettings::self()->askForCommentWhenReactingToInvitation() == MessageViewer::MessageViewerSettings::EnumAskForCommentWhenReactingToInvitation::AlwaysAsk))) { return true; } return false; } }; class Plugin : public QObject, public MessageViewer::MessagePartRenderPlugin { Q_OBJECT Q_INTERFACES(MessageViewer::MessagePartRenderPlugin) Q_PLUGIN_METADATA(IID "com.kde.messageviewer.bodypartformatter" FILE "text_calendar.json") public: MessageViewer::MessagePartRendererBase *renderer(int idx) override { if (idx < 2) { return new Formatter(); } else { return nullptr; } } const MessageViewer::Interface::BodyPartURLHandler *urlHandler(int idx) const override { if (idx == 0) { return new UrlHandler(); } else { return nullptr; } } }; } #include "text_calendar.moc" diff --git a/plugins/messageviewer/bodypartformatter/semantic/semanticurlhandler.cpp b/plugins/messageviewer/bodypartformatter/semantic/semanticurlhandler.cpp index 13d4774a..ffcf7288 100644 --- a/plugins/messageviewer/bodypartformatter/semantic/semanticurlhandler.cpp +++ b/plugins/messageviewer/bodypartformatter/semantic/semanticurlhandler.cpp @@ -1,471 +1,471 @@ /* Copyright (c) 2017 Volker Krause This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "semanticurlhandler.h" #include "semanticmemento.h" #include "semantic_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KItinerary; SemanticUrlHandler::SemanticUrlHandler() { m_appPath = QStandardPaths::findExecutable(QStringLiteral("itinerary")); } QString SemanticUrlHandler::name() const { return QStringLiteral("SemanticUrlHandler"); } static bool canAddToCalendar(SemanticMemento *m) { for (const auto &d : m->data()) { if (JsonLd::isA(d.reservations.at(0))) { const auto f = d.reservations.at(0).value().reservationFor().value(); if (f.departureTime().isValid() && f.arrivalTime().isValid()) { return true; } continue; } else if (SortUtil::startDateTime(d.reservations.at(0)).isValid()) { return true; } } return false; } bool SemanticUrlHandler::handleClick(MessageViewer::Viewer *viewerInstance, MimeTreeParser::Interface::BodyPart *part, const QString &path) const { Q_UNUSED(viewerInstance); if (path == QLatin1String("semanticAction")) { const auto m = memento(part); if (!m || !m->hasData()) { qCWarning(SEMANTIC_LOG) << "sementic action: data not found"; return true; } handleContextMenuRequest(part, path, QCursor::pos()); return true; } if (path.startsWith(QLatin1String("semanticExpand?"))) { auto idx = path.midRef(15).toInt(); auto m = memento(part); m->toggleExpanded(idx); const auto nodeHelper = part->nodeHelper(); emit nodeHelper->update(MimeTreeParser::Delayed); return true; } return false; } static QString escapePlaceName(const QString &name) { return QString(name).replace(QLatin1Char('&'), QLatin1String("&&")); // avoid & being turned into an action accelerator; } static void addGoToMapAction(QMenu *menu, const GeoCoordinates &geo, const QString &placeName, int zoom = 17) { if (geo.isValid()) { auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("map-symbolic")), i18n("Show \'%1\' On Map", escapePlaceName(placeName))); QObject::connect(action, &QAction::triggered, menu, [geo, zoom]() { QUrl url; url.setScheme(QStringLiteral("https")); url.setHost(QStringLiteral("www.openstreetmap.org")); url.setPath(QStringLiteral("/")); const QString fragment = QLatin1String("map=") + QString::number(zoom) + QLatin1Char('/') + QString::number(geo.latitude()) + QLatin1Char('/') + QString::number(geo.longitude()); url.setFragment(fragment); QDesktopServices::openUrl(url); }); } } static void addGoToMapAction(QMenu *menu, const PostalAddress &addr, const QString &placeName) { if (!addr.addressLocality().isEmpty()) { auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("map-symbolic")), i18n("Show \'%1\' On Map", escapePlaceName(placeName))); QObject::connect(action, &QAction::triggered, menu, [addr]() { QUrl url; url.setScheme(QStringLiteral("https")); url.setHost(QStringLiteral("www.openstreetmap.org")); url.setPath(QStringLiteral("/search")); const QString queryString = addr.streetAddress() + QLatin1String(", ") + addr.postalCode() + QLatin1Char(' ') + addr.addressLocality() + QLatin1String(", ") + addr.addressCountry(); QUrlQuery query; query.addQueryItem(QStringLiteral("query"), queryString); url.setQuery(query); QDesktopServices::openUrl(url); }); } } static void addGoToMapAction(QMenu *menu, const QVariant &place, QSet &places) { const auto name = LocationUtil::name(place); if (places.contains(name)) { return; } places.insert(name); const auto geo = LocationUtil::geo(place); const auto zoom = JsonLd::isA(place) ? 12 : 17; if (geo.isValid()) { addGoToMapAction(menu, geo, name, zoom); } else { addGoToMapAction(menu, LocationUtil::address(place), name); } } bool SemanticUrlHandler::handleContextMenuRequest(MimeTreeParser::Interface::BodyPart *part, const QString &path, const QPoint &p) const { Q_UNUSED(part); if (path != QLatin1String("semanticAction")) { return false; } const auto m = memento(part); if (!m || !m->hasData()) { return false; } const auto date = dateForReservation(m); QMenu menu; QAction *action = nullptr; if (date.isValid()) { action = menu.addAction(QIcon::fromTheme(QStringLiteral("view-calendar")), i18n("Show Calendar")); QObject::connect(action, &QAction::triggered, this, [this, date](){ showCalendar(date); }); } action = menu.addAction(QIcon::fromTheme(QStringLiteral("appointment-new")), i18n("Add To Calendar")); action->setEnabled(canAddToCalendar(m)); QObject::connect(action, &QAction::triggered, this, [this, m](){ addToCalendar(m); }); QSet places; for (const auto &d : m->data()) { const auto res = d.reservations.at(0); // for multi-traveler reservations all subsequent ones are equal regarding what we are interested here if (LocationUtil::isLocationChange(res)) { const auto dep = LocationUtil::departureLocation(res); addGoToMapAction(&menu, dep, places); const auto arr = LocationUtil::arrivalLocation(res); addGoToMapAction(&menu, arr, places); } else { const auto loc = LocationUtil::location(res); addGoToMapAction(&menu, loc, places); } } if (!m_appPath.isEmpty()) { menu.addSeparator(); action = menu.addAction(QIcon::fromTheme(QStringLiteral("map-globe")), i18n("Import into KDE Itinerary")); QObject::connect(action, &QAction::triggered, this, [this, part]() { openInApp(part); }); } QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect"), QStringLiteral("org.kde.kdeconnect.daemon"), QStringLiteral( "devices")); msg.setArguments({true, true}); QDBusPendingReply reply = QDBusConnection::sessionBus().asyncCall(msg); reply.waitForFinished(); if (reply.isValid()) { for (const QString &deviceId : reply.value()) { QDBusInterface deviceIface(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect/devices/") + deviceId, QStringLiteral("org.kde.kdeconnect.device")); QDBusReply pluginReply = deviceIface.call(QStringLiteral("hasPlugin"), QLatin1String("kdeconnect_share")); if (pluginReply.value()) { action = menu.addAction(QIcon::fromTheme(QStringLiteral("kdeconnect")), i18n("Send to %1", deviceIface.property("name").toString())); QObject::connect(action, &QAction::triggered, this, [this, part, deviceId]() { openWithKDEConnect(part, deviceId); }); } } } menu.exec(p); return true; } QString SemanticUrlHandler::statusBarMessage(MimeTreeParser::Interface::BodyPart *part, const QString &path) const { Q_UNUSED(part); if (path == QLatin1String("semanticAction")) { return i18n("Add reservation to your calendar."); } return {}; } SemanticMemento *SemanticUrlHandler::memento(MimeTreeParser::Interface::BodyPart *part) const { const auto node = part->content()->topLevel(); const auto nodeHelper = part->nodeHelper(); if (!nodeHelper || !node) { return nullptr; } return dynamic_cast(nodeHelper->bodyPartMemento(node->topLevel(), "org.kde.messageviewer.semanticData")); } QDate SemanticUrlHandler::dateForReservation(SemanticMemento *memento) const { for (const auto &d : memento->data()) { const auto dt = SortUtil::startDateTime(d.reservations.at(0)); if (dt.isValid()) { return dt.date(); } } return {}; } void SemanticUrlHandler::showCalendar(const QDate &date) const { // ensure KOrganizer or Kontact are running QString error, dbusService; const auto result = KDBusServiceStarter::self()->findServiceFor(QStringLiteral("DBUS/Organizer"), {}, &error, &dbusService) == 0; if (!result) { qCWarning(SEMANTIC_LOG) << "Failed to start KOrganizer" << error << dbusService; } // switch to KOrganizer if we are using Kontact std::unique_ptr kontactIface( new QDBusInterface(QStringLiteral("org.kde.kontact"), QStringLiteral("/KontactInterface"), QStringLiteral("org.kde.kontact.KontactInterface"), QDBusConnection::sessionBus())); if (kontactIface->isValid()) { kontactIface->call(QStringLiteral("selectPlugin"), QStringLiteral("kontact_korganizerplugin")); } // select the date of the reservation std::unique_ptr korgIface( new QDBusInterface(QStringLiteral("org.kde.korganizer"), QStringLiteral("/Calendar"), QStringLiteral("org.kde.Korganizer.Calendar"), QDBusConnection::sessionBus())); if (!korgIface->isValid()) { qCWarning(SEMANTIC_LOG) << "Calendar interface is not valid! " << korgIface->lastError().message(); return; } korgIface->call(QStringLiteral("showEventView")); korgIface->call(QStringLiteral("showDate"), date); } static void attachPass(const KCalCore::Event::Ptr &event, const QVector &reservations, SemanticMemento *memento) { for (const auto &reservation : reservations) { if (!JsonLd::canConvert(reservation)) { return; } const auto res = JsonLd::convert(reservation); const auto data = memento->rawPassData(res.pkpassPassTypeIdentifier(), res.pkpassSerialNumber()); if (data.isEmpty()) { return; } event->deleteAttachments(QStringLiteral("application/vnd.apple.pkpass")); using namespace KCalCore; - Attachment::Ptr att(new Attachment(data.toBase64(), QStringLiteral("application/vnd.apple.pkpass"))); - att->setLabel(i18n("Boarding Pass")); // TODO add passenger name after string freeze is lifted + Attachment att(data.toBase64(), QStringLiteral("application/vnd.apple.pkpass")); + att.setLabel(i18n("Boarding Pass")); // TODO add passenger name after string freeze is lifted event->addAttachment(att); } } void SemanticUrlHandler::addToCalendar(SemanticMemento *memento) const { using namespace KCalCore; const auto calendar = CalendarSupport::calendarSingleton(true); for (const auto &d : memento->data()) { auto event = d.event; if (!event) { event.reset(new KCalCore::Event); CalendarHandler::fillEvent(d.reservations, event); if (!event->dtStart().isValid() || !event->dtEnd().isValid() || event->summary().isEmpty()) { continue; } attachPass(event, d.reservations, memento); calendar->addEvent(event); } else { event->startUpdates(); CalendarHandler::fillEvent(d.reservations, event); event->endUpdates(); attachPass(event, d.reservations, memento); calendar->modifyIncidence(event); } } } void SemanticUrlHandler::openInApp(MimeTreeParser::Interface::BodyPart *part) const { QTemporaryFile f(QStringLiteral("itinerary.XXXXXX.jsonld")); if (!f.open()) { qCWarning(SEMANTIC_LOG) << "Failed to open temporary file:" << f.errorString(); return; } const auto m = memento(part); const auto extractedData = m->data(); QVector data; data.reserve(extractedData.size()); for (const auto &d : m->data()) { data += d.reservations; } f.write(QJsonDocument(JsonLdDocument::toJson(data)).toJson()); f.close(); part->nodeHelper()->addTempFile(f.fileName()); f.setAutoRemove(false); QStringList args(f.fileName()); // add pkpass attachments for (const auto &elem : data) { if (!JsonLd::canConvert(elem)) { continue; } const auto res = JsonLd::convert(elem); const auto b = m->rawPassData(res.pkpassPassTypeIdentifier(), res.pkpassSerialNumber()); if (b.isEmpty()) { continue; } QTemporaryFile f(QStringLiteral("itinerary.XXXXXX.pkpass")); if (!f.open()) { qCWarning(SEMANTIC_LOG) << "Failed to open temporary file:" << f.errorString(); return; } f.write(b); f.close(); part->nodeHelper()->addTempFile(f.fileName()); f.setAutoRemove(false); args.push_back(f.fileName()); } QProcess::startDetached(m_appPath, args); } void SemanticUrlHandler::openWithKDEConnect(MimeTreeParser::Interface::BodyPart *part, const QString &deviceId) const { QTemporaryFile f(QStringLiteral("itinerary.XXXXXX.jsonld")); if (!f.open()) { qCWarning(SEMANTIC_LOG) << "Failed to open temporary file:" << f.errorString(); return; } const auto m = memento(part); const auto extractedData = m->data(); QVector data; data.reserve(extractedData.size()); for (const auto &d : m->data()) { data += d.reservations; } f.write(QJsonDocument(JsonLdDocument::toJson(data)).toJson()); f.close(); part->nodeHelper()->addTempFile(f.fileName()); f.setAutoRemove(false); QDBusInterface remoteApp(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/MainApplication"), QStringLiteral("org.qtproject.Qt.QCoreApplication")); QVersionNumber kdeconnectVersion = QVersionNumber::fromString(remoteApp.property("applicationVersion").toString()); QString method; if (kdeconnectVersion >= QVersionNumber(1, 4, 0)) { method = QStringLiteral("openFile"); } else { method = QStringLiteral("shareUrl"); } QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect/devices/") + deviceId + QStringLiteral("/share"), QStringLiteral( "org.kde.kdeconnect.device.share"), method); msg.setArguments({QUrl::fromLocalFile(f.fileName()).toString()}); QDBusConnection::sessionBus().send(msg); // add pkpass attachments for (const auto &elem : data) { if (!JsonLd::canConvert(elem)) { continue; } const auto res = JsonLd::convert(elem); const auto b = m->rawPassData(res.pkpassPassTypeIdentifier(), res.pkpassSerialNumber()); if (b.isEmpty()) { continue; } QTemporaryFile f(QStringLiteral("itinerary.XXXXXX.pkpass")); if (!f.open()) { qCWarning(SEMANTIC_LOG) << "Failed to open temporary file:" << f.errorString(); return; } f.write(b); f.close(); part->nodeHelper()->addTempFile(f.fileName()); f.setAutoRemove(false); msg.setArguments({QUrl::fromLocalFile(f.fileName()).toString()}); QDBusConnection::sessionBus().send(msg); } } diff --git a/plugins/messageviewerplugins/createeventplugin/eventedit.cpp b/plugins/messageviewerplugins/createeventplugin/eventedit.cpp index 5b6e1ece..2cb5b7d9 100644 --- a/plugins/messageviewerplugins/createeventplugin/eventedit.cpp +++ b/plugins/messageviewerplugins/createeventplugin/eventedit.cpp @@ -1,366 +1,366 @@ /* Copyright (C) 2014-2019 Montel Laurent This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "eventedit.h" #include "createeventplugin_debug.h" #include "globalsettings_messageviewer.h" #include "eventdatetimewidget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace MessageViewer { QAbstractItemModel *_k_eventEditStubModel = nullptr; } using namespace MessageViewer; EventEdit::EventEdit(QWidget *parent) : QWidget(parent) { QVBoxLayout *vbox = new QVBoxLayout(this); vbox->setContentsMargins(5, 5, 5, 5); vbox->setSpacing(2); QHBoxLayout *hbox = new QHBoxLayout; hbox->setContentsMargins(0, 0, 0, 0); hbox->setSpacing(2); vbox->addLayout(hbox); QLabel *lab = new QLabel(i18n("Event:"), this); hbox->addWidget(lab); mEventEdit = new QLineEdit(this); mEventEdit->setClearButtonEnabled(true); mEventEdit->setObjectName(QStringLiteral("eventedit")); mEventEdit->setFocus(); connect(mEventEdit, &QLineEdit::returnPressed, this, &EventEdit::slotReturnPressed); connect(mEventEdit, &QLineEdit::textChanged, this, &EventEdit::slotUpdateButtons); hbox->addWidget(mEventEdit); hbox->addSpacing(5); mCollectionCombobox = new Akonadi::CollectionComboBox(_k_eventEditStubModel, this); mCollectionCombobox->setAccessRightsFilter(Akonadi::Collection::CanCreateItem); mCollectionCombobox->setMinimumWidth(250); mCollectionCombobox->setMimeTypeFilter(QStringList() << KCalCore::Event::eventMimeType()); mCollectionCombobox->setObjectName(QStringLiteral("akonadicombobox")); #ifndef QT_NO_ACCESSIBILITY mCollectionCombobox->setAccessibleDescription(i18n("Calendar where the new event will be stored.")); #endif mCollectionCombobox->setToolTip(i18n("Calendar where the new event will be stored")); connect(mCollectionCombobox, qOverload(&Akonadi::CollectionComboBox::currentIndexChanged), this, &EventEdit::slotCollectionChanged); connect(mCollectionCombobox, qOverload(&Akonadi::CollectionComboBox::activated), this, &EventEdit::slotCollectionChanged); connect(mCollectionCombobox->model(), &QAbstractItemModel::rowsInserted, this, &EventEdit::comboboxRowInserted); hbox->addWidget(mCollectionCombobox); hbox = new QHBoxLayout; hbox->setContentsMargins(0, 0, 0, 0); hbox->setSpacing(2); vbox->addLayout(hbox); lab = new QLabel(i18n("Start:"), this); hbox->addWidget(lab); QDateTime currentDateTime = QDateTime::currentDateTime(); mStartDateTimeEdit = new EventDateTimeWidget(this); mStartDateTimeEdit->setObjectName(QStringLiteral("startdatetimeedit")); mStartDateTimeEdit->setDateTime(currentDateTime); #ifndef QT_NO_ACCESSIBILITY mStartDateTimeEdit->setAccessibleDescription(i18n("Select start time for event.")); #endif connect(mStartDateTimeEdit, &EventDateTimeWidget::dateTimeChanged, this, &EventEdit::slotStartDateTimeChanged); hbox->addWidget(mStartDateTimeEdit); hbox->addSpacing(5); lab = new QLabel(i18n("End:"), this); hbox->addWidget(lab); mEndDateTimeEdit = new EventDateTimeWidget(this); mEndDateTimeEdit->setObjectName(QStringLiteral("enddatetimeedit")); mEndDateTimeEdit->setDateTime(currentDateTime.addSecs(3600)); #ifndef QT_NO_ACCESSIBILITY mEndDateTimeEdit->setAccessibleDescription(i18n("Select end time for event.")); #endif connect(mEndDateTimeEdit, &EventDateTimeWidget::dateTimeChanged, this, &EventEdit::slotEndDateTimeChanged); hbox->addWidget(mEndDateTimeEdit); hbox->addStretch(1); hbox = new QHBoxLayout; hbox->setSpacing(2); hbox->setContentsMargins(0, 0, 0, 0); vbox->addLayout(hbox); hbox->addStretch(1); mSaveButton = new QPushButton(QIcon::fromTheme(QStringLiteral("appointment-new")), i18n("&Save"), this); mSaveButton->setObjectName(QStringLiteral("save-button")); mSaveButton->setEnabled(false); #ifndef QT_NO_ACCESSIBILITY mSaveButton->setAccessibleDescription(i18n("Create new event and close this widget.")); #endif connect(mSaveButton, &QPushButton::clicked, this, &EventEdit::slotReturnPressed); hbox->addWidget(mSaveButton); mOpenEditorButton = new QPushButton(i18n("Open &editor..."), this); #ifndef QT_NO_ACCESSIBILITY mOpenEditorButton->setAccessibleDescription(i18n("Open event editor, where more details can be changed.")); #endif mOpenEditorButton->setObjectName(QStringLiteral("open-editor-button")); mOpenEditorButton->setEnabled(false); connect(mOpenEditorButton, &QPushButton::clicked, this, &EventEdit::slotOpenEditor); hbox->addWidget(mOpenEditorButton); QPushButton *btn = new QPushButton(this); KGuiItem::assign(btn, KStandardGuiItem::cancel()); btn->setObjectName(QStringLiteral("close-button")); #ifndef QT_NO_ACCESSIBILITY btn->setAccessibleDescription(i18n("Close the widget for creating new events.")); #endif connect(btn, &QPushButton::clicked, this, &EventEdit::slotCloseWidget); hbox->addWidget(btn); readConfig(); setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed)); installEventFilter(this); mCollectionCombobox->installEventFilter(this); } EventEdit::~EventEdit() { writeConfig(); } void EventEdit::comboboxRowInserted() { slotUpdateButtons(mEventEdit->text()); } void EventEdit::writeConfig() { const Akonadi::Collection col = mCollectionCombobox->currentCollection(); // col might not be valid if the collection wasn't found yet (the combo is async), skip saving in that case. if (col.isValid() && col.id() != MessageViewer::MessageViewerSettingsBase::self()->lastEventSelectedFolder()) { MessageViewer::MessageViewerSettingsBase::self()->setLastEventSelectedFolder(col.id()); MessageViewer::MessageViewerSettingsBase::self()->save(); } } void EventEdit::slotUpdateButtons(const QString &subject) { const bool subjectIsNotEmpty = !subject.trimmed().isEmpty(); const bool collectionComboboxEmpty = (mCollectionCombobox->count() < 1); mSaveButton->setEnabled(subjectIsNotEmpty && !collectionComboboxEmpty); mOpenEditorButton->setEnabled(subjectIsNotEmpty && !collectionComboboxEmpty); } void EventEdit::showEventEdit() { mEventEdit->setFocus(); QDateTime currentDateTime = QDateTime::currentDateTime(); mStartDateTimeEdit->setDateTime(currentDateTime); mEndDateTimeEdit->setDateTime(currentDateTime.addSecs(3600)); show(); } void EventEdit::readConfig() { const qint64 id = MessageViewer::MessageViewerSettingsBase::self()->lastEventSelectedFolder(); if (id >= 0) { mCollectionCombobox->setDefaultCollection(Akonadi::Collection(id)); } } Akonadi::Collection EventEdit::collection() const { return mCollection; } void EventEdit::slotCollectionChanged(int /*index*/) { setCollection(mCollectionCombobox->currentCollection()); } void EventEdit::setCollection(const Akonadi::Collection &value) { if (mCollection != value) { mCollection = value; Q_EMIT collectionChanged(mCollection); } } KMime::Message::Ptr EventEdit::message() const { return mMessage; } void EventEdit::setMessage(const KMime::Message::Ptr &value) { if (mMessage != value) { mMessage = value; const KMime::Headers::Subject *const subject = mMessage ? mMessage->subject(false) : nullptr; if (subject) { mEventEdit->setText(subject->asUnicodeString()); mEventEdit->selectAll(); mEventEdit->setFocus(); } else { mEventEdit->clear(); } Q_EMIT messageChanged(mMessage); } } void EventEdit::slotCloseWidget() { if (isVisible()) { writeConfig(); mEventEdit->clear(); mMessage = KMime::Message::Ptr(); hide(); } } void EventEdit::slotReturnPressed() { if (!mMessage) { qCDebug(CREATEEVENTPLUGIN_LOG) << " Message is null"; return; } const Akonadi::Collection collection = mCollectionCombobox->currentCollection(); if (!collection.isValid()) { qCDebug(CREATEEVENTPLUGIN_LOG) << " Collection is not valid"; return; } const QDateTime dtstart = mStartDateTimeEdit->dateTime(); const QDateTime dtend = mEndDateTimeEdit->dateTime(); if (!dtstart.isValid() || !dtend.isValid()) { qCDebug(CREATEEVENTPLUGIN_LOG) << " date is not valid !"; return; } if (!mEventEdit->text().trimmed().isEmpty()) { KCalCore::Event::Ptr event = createEventItem(); Q_EMIT createEvent(event, collection); mEventEdit->clear(); hide(); } } bool EventEdit::eventFilter(QObject *object, QEvent *e) { // Close the bar when pressing Escape. // Not using a QShortcut for this because it could conflict with // window-global actions (e.g. Emil Sedgh binds Esc to "close tab"). // With a shortcut override we can catch this before it gets to kactions. const bool shortCutOverride = (e->type() == QEvent::ShortcutOverride); if (shortCutOverride) { QKeyEvent *kev = static_cast(e); if (kev->key() == Qt::Key_Escape) { e->accept(); slotCloseWidget(); return true; } else if (kev->key() == Qt::Key_Enter || kev->key() == Qt::Key_Return || kev->key() == Qt::Key_Space) { e->accept(); if (object == mCollectionCombobox) { mCollectionCombobox->showPopup(); } return true; } } return QWidget::eventFilter(object, e); } void EventEdit::slotEndDateTimeChanged(const QDateTime &newDateTime) { if (!newDateTime.isValid()) { return; } QDateTime currentDateTime = QDateTime::currentDateTime(); if (newDateTime.date() > currentDateTime.date()) { QDateTime newDateDate = newDateTime; newDateDate.setTime(QTime(0, 0, 0)); mEndDateTimeEdit->setMinimumDateTime(newDateDate); } } void EventEdit::slotStartDateTimeChanged(const QDateTime &newDateTime) { if (!newDateTime.isValid()) { return; } if (mEndDateTimeEdit->date() == newDateTime.date() && mEndDateTimeEdit->time() < newDateTime.time()) { mEndDateTimeEdit->setTime(newDateTime.time()); } if (mEndDateTimeEdit->date() < newDateTime.date()) { mEndDateTimeEdit->setDate(newDateTime.date()); } mEndDateTimeEdit->setMinimumDateTime(newDateTime); } KCalCore::Event::Ptr EventEdit::createEventItem() { - KCalCore::Attachment::Ptr attachment(new KCalCore::Attachment(mMessage->encodedContent().toBase64(), KMime::Message::mimeType())); + KCalCore::Attachment attachment(mMessage->encodedContent().toBase64(), KMime::Message::mimeType()); const KMime::Headers::Subject *const subject = mMessage->subject(false); if (subject) { - attachment->setLabel(subject->asUnicodeString()); + attachment.setLabel(subject->asUnicodeString()); } KCalCore::Event::Ptr event(new KCalCore::Event); event->setSummary(mEventEdit->text()); event->setDtStart(mStartDateTimeEdit->dateTime()); event->setDtEnd(mEndDateTimeEdit->dateTime()); event->addAttachment(attachment); return event; } void EventEdit::slotOpenEditor() { KCalCore::Event::Ptr event = createEventItem(); Akonadi::Item item; item.setPayload(event); item.setMimeType(KCalCore::Event::eventMimeType()); IncidenceEditorNG::IncidenceDialog *dlg = IncidenceEditorNG::IncidenceDialogFactory::create(true, KCalCore::IncidenceBase::TypeEvent, nullptr, this); connect(dlg, &IncidenceEditorNG::IncidenceDialog::finished, this, &EventEdit::slotCloseWidget); dlg->load(item); dlg->open(); } diff --git a/plugins/messageviewerplugins/createtodoplugin/todoedit.cpp b/plugins/messageviewerplugins/createtodoplugin/todoedit.cpp index 09d7260d..b51fda2e 100644 --- a/plugins/messageviewerplugins/createtodoplugin/todoedit.cpp +++ b/plugins/messageviewerplugins/createtodoplugin/todoedit.cpp @@ -1,323 +1,323 @@ /* Copyright (C) 2014-2019 Montel Laurent This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "todoedit.h" #include "globalsettings_messageviewer.h" #include "createtodoplugin_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace MessageViewer { QAbstractItemModel *_k_todoEditStubModel = nullptr; } using namespace MessageViewer; TodoEdit::TodoEdit(QWidget *parent) : QWidget(parent) { QVBoxLayout *vbox = new QVBoxLayout(this); vbox->setContentsMargins(5, 5, 5, 5); vbox->setSpacing(2); mMsgWidget = new KMessageWidget(this); mMsgWidget->setCloseButtonVisible(true); mMsgWidget->setMessageType(KMessageWidget::Positive); mMsgWidget->setObjectName(QStringLiteral("msgwidget")); mMsgWidget->setWordWrap(true); mMsgWidget->setVisible(false); vbox->addWidget(mMsgWidget); QHBoxLayout *hbox = new QHBoxLayout; hbox->setContentsMargins(0, 0, 0, 0); hbox->setSpacing(2); vbox->addLayout(hbox); QLabel *lab = new QLabel(i18n("Todo:"), this); hbox->addWidget(lab); mNoteEdit = new QLineEdit(this); mNoteEdit->setClearButtonEnabled(true); mNoteEdit->setObjectName(QStringLiteral("noteedit")); mNoteEdit->setFocus(); connect(mNoteEdit, &QLineEdit::textChanged, this, &TodoEdit::slotTextEdited); connect(mNoteEdit, &QLineEdit::returnPressed, this, &TodoEdit::slotReturnPressed); hbox->addWidget(mNoteEdit, 1); hbox->addSpacing(5); mCollectionCombobox = new Akonadi::CollectionComboBox(_k_todoEditStubModel, this); mCollectionCombobox->setAccessRightsFilter(Akonadi::Collection::CanCreateItem); mCollectionCombobox->setMinimumWidth(250); mCollectionCombobox->setMimeTypeFilter(QStringList() << KCalCore::Todo::todoMimeType()); mCollectionCombobox->setObjectName(QStringLiteral("akonadicombobox")); connect(mCollectionCombobox->model(), &QAbstractItemModel::rowsInserted, this, &TodoEdit::comboboxRowInserted); #ifndef QT_NO_ACCESSIBILITY mCollectionCombobox->setAccessibleDescription(i18n("Todo list where the new task will be stored.")); #endif mCollectionCombobox->setToolTip(i18n("Todo list where the new task will be stored")); connect(mCollectionCombobox, qOverload(&Akonadi::CollectionComboBox::currentIndexChanged), this, &TodoEdit::slotCollectionChanged); connect(mCollectionCombobox, qOverload(&Akonadi::CollectionComboBox::activated), this, &TodoEdit::slotCollectionChanged); hbox->addWidget(mCollectionCombobox); hbox = new QHBoxLayout; hbox->setContentsMargins(0, 0, 0, 0); hbox->setSpacing(2); vbox->addLayout(hbox); hbox->addStretch(1); mSaveButton = new QPushButton(QIcon::fromTheme(QStringLiteral("task-new")), i18n("&Save"), this); mSaveButton->setObjectName(QStringLiteral("save-button")); mSaveButton->setEnabled(false); #ifndef QT_NO_ACCESSIBILITY mSaveButton->setAccessibleDescription(i18n("Create new todo and close this widget.")); #endif connect(mSaveButton, &QPushButton::clicked, this, &TodoEdit::slotReturnPressed); hbox->addWidget(mSaveButton); mOpenEditorButton = new QPushButton(i18n("Open &Editor..."), this); mOpenEditorButton->setObjectName(QStringLiteral("open-editor-button")); #ifndef QT_NO_ACCESSIBILITY mOpenEditorButton->setAccessibleDescription(i18n("Open todo editor, where more details can be changed.")); #endif mOpenEditorButton->setEnabled(false); connect(mOpenEditorButton, &QPushButton::clicked, this, &TodoEdit::slotOpenEditor); hbox->addWidget(mOpenEditorButton); QPushButton *btn = new QPushButton(this); KGuiItem::assign(btn, KStandardGuiItem::cancel()); btn->setObjectName(QStringLiteral("close-button")); #ifndef QT_NO_ACCESSIBILITY btn->setAccessibleDescription(i18n("Close the widget for creating new todos.")); #endif connect(btn, &QPushButton::clicked, this, &TodoEdit::slotCloseWidget); hbox->addWidget(btn); readConfig(); setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed)); mCollectionCombobox->installEventFilter(this); installEventFilter(this); } TodoEdit::~TodoEdit() { writeConfig(); } void TodoEdit::comboboxRowInserted() { updateButtons(mNoteEdit->text()); } void TodoEdit::updateButtons(const QString &subject) { const bool subjectIsNotEmpty = !subject.trimmed().isEmpty(); const bool collectionComboboxEmpty = (mCollectionCombobox->count() < 1); mSaveButton->setEnabled(subjectIsNotEmpty && !collectionComboboxEmpty); mOpenEditorButton->setEnabled(subjectIsNotEmpty && !collectionComboboxEmpty); } void TodoEdit::showToDoWidget() { const KMime::Headers::Subject *const subject = mMessage ? mMessage->subject(false) : nullptr; if (subject) { bool isSentFolder = false; if (mCurrentCollection.isValid()) { isSentFolder = (Akonadi::SpecialMailCollections::self()->defaultCollection(Akonadi::SpecialMailCollections::SentMail) == mCurrentCollection); } mNoteEdit->setText(isSentFolder ? i18n("Check I received a reply about \"%1\"", subject->asUnicodeString()) : i18n("Reply to \"%1\"", subject->asUnicodeString())); mNoteEdit->selectAll(); mNoteEdit->setFocus(); } else { mNoteEdit->clear(); } mNoteEdit->setFocus(); show(); } void TodoEdit::setCurrentCollection(const Akonadi::Collection &col) { mCurrentCollection = col; } void TodoEdit::writeConfig() { const Akonadi::Collection col = mCollectionCombobox->currentCollection(); // col might not be valid if the collection wasn't found yet (the combo is async), skip saving in that case if (col.isValid() && col.id() != MessageViewer::MessageViewerSettingsBase::self()->lastSelectedFolder()) { MessageViewer::MessageViewerSettingsBase::self()->setLastSelectedFolder(col.id()); MessageViewer::MessageViewerSettingsBase::self()->save(); } } void TodoEdit::readConfig() { const qint64 id = MessageViewer::MessageViewerSettingsBase::self()->lastSelectedFolder(); if (id != -1) { mCollectionCombobox->setDefaultCollection(Akonadi::Collection(id)); } } Akonadi::Collection TodoEdit::collection() const { return mCollection; } void TodoEdit::slotCollectionChanged(int /*index*/) { setCollection(mCollectionCombobox->currentCollection()); } void TodoEdit::setCollection(const Akonadi::Collection &value) { if (mCollection != value) { mCollection = value; Q_EMIT collectionChanged(mCollection); } } KMime::Message::Ptr TodoEdit::message() const { return mMessage; } void TodoEdit::setMessage(const KMime::Message::Ptr &value) { if (mMessage != value) { mMessage = value; Q_EMIT messageChanged(mMessage); } } void TodoEdit::slotCloseWidget() { if (isVisible()) { writeConfig(); mNoteEdit->clear(); mMessage = KMime::Message::Ptr(); mMsgWidget->hide(); hide(); } } void TodoEdit::slotReturnPressed() { if (!mMessage) { qCDebug(CREATETODOPLUGIN_LOG) << " Message is null"; return; } const Akonadi::Collection collection = mCollectionCombobox->currentCollection(); if (!collection.isValid()) { qCDebug(CREATETODOPLUGIN_LOG) << " Collection is not valid"; return; } if (!mNoteEdit->text().trimmed().isEmpty()) { mMsgWidget->setText(i18nc("%1 is summary of the todo, %2 is name of the folder in which it is stored", "New todo '%1' was added to task list '%2'", mNoteEdit->text(), collection.displayName())); KCalCore::Todo::Ptr todo = createTodoItem(); mNoteEdit->clear(); // We don't hide the widget here, so that multiple todo's can be added Q_EMIT createTodo(todo, collection); mMsgWidget->animatedShow(); } } KCalCore::Todo::Ptr TodoEdit::createTodoItem() { KCalCore::Todo::Ptr todo(new KCalCore::Todo); todo->setSummary(mNoteEdit->text()); - KCalCore::Attachment::Ptr attachment(new KCalCore::Attachment(mMessage->encodedContent().toBase64(), KMime::Message::mimeType())); + KCalCore::Attachment attachment(mMessage->encodedContent().toBase64(), KMime::Message::mimeType()); const KMime::Headers::Subject *const subject = mMessage->subject(false); if (subject) { - attachment->setLabel(subject->asUnicodeString()); + attachment.setLabel(subject->asUnicodeString()); } todo->addAttachment(attachment); return todo; } bool TodoEdit::eventFilter(QObject *object, QEvent *e) { // Close the bar when pressing Escape. // Not using a QShortcut for this because it could conflict with // window-global actions (e.g. Emil Sedgh binds Esc to "close tab"). // With a shortcut override we can catch this before it gets to kactions. const bool shortCutOverride = (e->type() == QEvent::ShortcutOverride); if (shortCutOverride || e->type() == QEvent::KeyPress) { QKeyEvent *kev = static_cast(e); if (kev->key() == Qt::Key_Escape) { e->accept(); slotCloseWidget(); return true; } else if (kev->key() == Qt::Key_Enter || kev->key() == Qt::Key_Return || kev->key() == Qt::Key_Space) { e->accept(); if (shortCutOverride) { return true; } if (object == mCollectionCombobox) { mCollectionCombobox->showPopup(); return true; } } } return QWidget::eventFilter(object, e); } void TodoEdit::slotOpenEditor() { KCalCore::Todo::Ptr event = createTodoItem(); Akonadi::Item item; item.setPayload(event); item.setMimeType(KCalCore::Todo::todoMimeType()); IncidenceEditorNG::IncidenceDialog *dlg = IncidenceEditorNG::IncidenceDialogFactory::create(true, KCalCore::IncidenceBase::TypeTodo, nullptr, this); connect(dlg, &IncidenceEditorNG::IncidenceDialog::finished, this, &TodoEdit::slotCloseWidget); dlg->load(item); dlg->open(); } void TodoEdit::slotTextEdited(const QString &subject) { updateButtons(subject); if (mMsgWidget->isVisible()) { mMsgWidget->hide(); } }