diff --git a/CMakeLists.txt b/CMakeLists.txt index 024a1787..b486fd08 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,130 +1,130 @@ cmake_minimum_required(VERSION 3.5) set(PIM_VERSION "5.14.45") if (POLICY CMP0053) cmake_policy(SET CMP0053 NEW) endif() project(Messagelib VERSION ${PIM_VERSION}) 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(KDEPIM_RUN_AKONADI_TEST "Enable autotest based on Akonadi." TRUE) option(MESSAGEVIEWER_EXPERIMENTAL_CONVERSATIONVIEW "Experimental conversationview (in progress)" FALSE) set(KF5_MIN_VERSION "5.70.0") set(MESSAGELIB_LIB_VERSION ${PIM_VERSION}) set(AKONADIMIME_LIB_VERSION "5.14.42") set(QT_REQUIRED_VERSION "5.13.0") set(AKONADI_VERSION "5.14.40") set(GRANTLEETHEME_LIB_VERSION "5.14.40") set(GRAVATAR_LIB_VERSION "5.14.41") -set(IDENTITYMANAGEMENT_LIB_VERSION "5.14.40") +set(IDENTITYMANAGEMENT_LIB_VERSION "5.14.41") set(KDEPIM_APPS_LIB_VERSION "5.14.40") set(KLDAP_LIB_VERSION "5.14.40") set(KMAILTRANSPORT_LIB_VERSION "5.14.40") set(KMBOX_LIB_VERSION "5.14.40") set(KMIME_LIB_VERSION "5.14.40") set(KPIMTEXTEDIT_LIB_VERSION "5.14.45") set(LIBKDEPIM_LIB_VERSION "5.14.41") set(LIBKLEO_LIB_VERSION "5.14.40") set(PIMCOMMON_LIB_VERSION "5.14.40") set(GPGME_LIB_VERSION "1.11.1") set(AKONADI_CONTACT_VERSION "5.14.40") set(ECM_VERSION ${KF5_MIN_VERSION}) find_package(ECM ${ECM_VERSION} CONFIG REQUIRED) set(CMAKE_MODULE_PATH ${Messagelib_SOURCE_DIR}/cmake/modules ${ECM_MODULE_PATH}) set(LIBRARY_NAMELINK) include(KDEInstallDirs) include(KDECMakeSettings) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(GenerateExportHeader) include(ECMSetupVersion) include(ECMGenerateHeaders) include(ECMGeneratePriFile) include(FeatureSummary) include(ECMQtDeclareLoggingCategory) include(ECMAddTests) find_package(Qt5 ${QT_REQUIRED_VERSION} CONFIG REQUIRED Gui Test) find_package(KF5Codecs ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5I18n ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5Mime ${KMIME_LIB_VERSION} CONFIG REQUIRED) find_package(KF5NewStuff ${KMIME_LIB_VERSION} CONFIG REQUIRED) find_package(QGpgme ${GPGME_LIB_VERSION} CONFIG REQUIRED) find_package(Qt5 ${QT_REQUIRED_VERSION} CONFIG REQUIRED Widgets Network PrintSupport WebEngine WebEngineWidgets) find_package(KF5Archive ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5Completion ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5Config ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5ConfigWidgets ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5IconThemes ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5ItemViews ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5JobWidgets ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5KIO ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5Service ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5Sonnet ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5TextWidgets ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5WidgetsAddons ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5XmlGui ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5SyntaxHighlighting ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5DBusAddons ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(Grantlee5 "5.2" CONFIG REQUIRED) #Use KF5_VERSION in the future find_package(KF5Akonadi ${AKONADI_VERSION} CONFIG REQUIRED) find_package(KF5AkonadiMime ${AKONADIMIME_LIB_VERSION} CONFIG REQUIRED) find_package(KF5Contacts ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5AkonadiContact ${AKONADI_CONTACT_VERSION} CONFIG REQUIRED) find_package(KF5GrantleeTheme ${GRANTLEETHEME_LIB_VERSION} CONFIG REQUIRED) find_package(KF5Gravatar ${GRAVATAR_LIB_VERSION} CONFIG REQUIRED) find_package(KF5IdentityManagement ${IDENTITYMANAGEMENT_LIB_VERSION} CONFIG REQUIRED) find_package(KF5KaddressbookGrantlee ${KDEPIM_APPS_LIB_VERSION} CONFIG REQUIRED) find_package(KF5Ldap ${KLDAP_LIB_VERSION} CONFIG REQUIRED) find_package(KF5LibkdepimAkonadi ${LIBKDEPIM_LIB_VERSION} CONFIG REQUIRED) find_package(KF5Libkleo ${LIBKLEO_LIB_VERSION} CONFIG REQUIRED) find_package(KF5MailTransportAkonadi ${KMAILTRANSPORT_LIB_VERSION} CONFIG REQUIRED) find_package(KF5Mbox ${KMBOX_LIB_VERSION} CONFIG REQUIRED) find_package(KF5PimCommonAkonadi ${PIMCOMMON_LIB_VERSION} CONFIG REQUIRED) find_package(KF5PimTextEdit ${KPIMTEXTEDIT_LIB_VERSION} CONFIG REQUIRED) find_package(KF5AkonadiSearch "5.14.40" CONFIG REQUIRED) set_package_properties(KF5AkonadiSearch PROPERTIES DESCRIPTION "The Akonadi Search libraries" URL "https://kde.org/" TYPE REQUIRED PURPOSE "Provides search capabilities in KMail and Akonadi") set(CMAKE_CXX_STANDARD 14) if (EXISTS "${CMAKE_SOURCE_DIR}/.git") add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x050e00) add_definitions(-DKF_DISABLE_DEPRECATED_BEFORE_AND_AT=0x054600) endif() if(BUILD_TESTING) add_definitions(-DBUILD_TESTING) endif() add_definitions(-DQT_NO_SIGNALS_SLOTS_KEYWORDS) add_definitions(-DQT_NO_EMIT) add_subdirectory(mimetreeparser) add_subdirectory(messageviewer) add_subdirectory(templateparser) add_subdirectory(messagecomposer) add_subdirectory(messagecore) add_subdirectory(messagelist) add_subdirectory(webengineviewer) ecm_qt_install_logging_categories( EXPORT MESSAGELIB FILE messagelib.categories DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR} ) feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/messagecomposer/autotests/composertest.cpp b/messagecomposer/autotests/composertest.cpp index 772aa183..e7dc6443 100644 --- a/messagecomposer/autotests/composertest.cpp +++ b/messagecomposer/autotests/composertest.cpp @@ -1,159 +1,159 @@ /* Copyright (c) 2009 Constantin Berzan Copyright (c) 2009 Leo Franchi 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 "composertest.h" #include "qtest_messagecomposer.h" #include #include using namespace KMime; #include #include #include #include using namespace MessageComposer; #include using MessageCore::AttachmentPart; QTEST_MAIN(ComposerTest) void ComposerTest::testAttachments() { Composer *composer = new Composer; fillComposerData(composer); AttachmentPart::Ptr attachment = AttachmentPart::Ptr(new AttachmentPart); attachment->setData("abc"); attachment->setMimeType("x-some/x-type"); composer->addAttachmentPart(attachment); QVERIFY(composer->exec()); QCOMPARE(composer->resultMessages().size(), 1); KMime::Message::Ptr message = composer->resultMessages().constFirst(); delete composer; composer = nullptr; // multipart/mixed { QVERIFY(message->contentType(false)); QCOMPARE(message->contentType()->mimeType(), QByteArray("multipart/mixed")); QCOMPARE(message->contents().count(), 2); // text/plain { Content *plain = message->contents().at(0); QVERIFY(plain->contentType(false)); QCOMPARE(plain->contentType()->mimeType(), QByteArray("text/plain")); } // x-some/x-type (attachment) { Content *plain = message->contents().at(1); QVERIFY(plain->contentType(false)); QCOMPARE(plain->contentType()->mimeType(), QByteArray("x-some/x-type")); } } } void ComposerTest::testAutoSave() { Composer *composer = new Composer; fillComposerData(composer); AttachmentPart::Ptr attachment = AttachmentPart::Ptr(new AttachmentPart); attachment->setData("abc"); attachment->setMimeType("x-some/x-type"); composer->addAttachmentPart(attachment); // This tests if autosave in crash mode works without invoking an event loop, since using an // event loop in the crash handler would be a pretty bad idea composer->setAutoSave(true); composer->start(); QVERIFY(composer->finished()); QCOMPARE(composer->resultMessages().size(), 1); delete composer; composer = nullptr; } void ComposerTest::testNonAsciiHeaders() { Composer *composer = new Composer; fillComposerData(composer); const QString mailbox = QStringLiteral(" "); const QString fromDisplayName = QStringLiteral("Hellö"); const QString toDisplayName = QStringLiteral("æſłĸð"); const QString ccDisplayName = QStringLiteral("Вася"); const QString bccDisplayName = QStringLiteral("ĸłſðđøþĸµ»«„¢þµ¢”«ł„·ĸ”"); const QString replyToDisplayName = QStringLiteral("æĸſłð˝đВасяðæĸđ"); const QString from = fromDisplayName + mailbox; const QString to = toDisplayName + mailbox; const QString cc = ccDisplayName + mailbox; const QString bcc = bccDisplayName + mailbox; const QStringList replyto = QStringList{replyToDisplayName + mailbox}; composer->infoPart()->setFrom(from); composer->infoPart()->setTo(QStringList() << to); composer->infoPart()->setCc(QStringList() << cc); composer->infoPart()->setBcc(QStringList() << bcc); composer->infoPart()->setReplyTo(replyto); QVERIFY(composer->exec()); QCOMPARE(composer->resultMessages().size(), 1); const KMime::Message::Ptr message = composer->resultMessages().constFirst(); message->assemble(); message->parse(); QCOMPARE(message->bcc(false)->displayNames().size(), 1); QCOMPARE(message->to(false)->displayNames().size(), 1); QCOMPARE(message->cc(false)->displayNames().size(), 1); QCOMPARE(message->from(false)->displayNames().size(), 1); QCOMPARE(message->replyTo(false)->displayNames().size(), 1); QCOMPARE(message->from()->displayNames().constFirst(), fromDisplayName); QCOMPARE(message->to()->displayNames().constFirst(), toDisplayName); QCOMPARE(message->cc()->displayNames().constFirst(), ccDisplayName); QCOMPARE(message->bcc()->displayNames().constFirst(), bccDisplayName); QCOMPARE(message->replyTo()->displayNames().constFirst(), replyToDisplayName); delete composer; composer = nullptr; } void ComposerTest::testBug271192() { const QString displayName = QStringLiteral("Интернет-компания example"); const QString mailbox = QStringLiteral("example@example.com"); Composer *composer = new Composer; fillComposerData(composer); composer->infoPart()->setTo(QStringList() << (displayName + QLatin1String(" <") + mailbox + QLatin1String(">"))); QVERIFY(composer->exec()); QCOMPARE(composer->resultMessages().size(), 1); - const KMime::Message::Ptr message = composer->resultMessages().first(); + const KMime::Message::Ptr message = composer->resultMessages().constFirst(); QCOMPARE(message->to()->displayNames().size(), 1); QCOMPARE(message->to()->displayNames().first().toUtf8(), displayName.toUtf8()); delete composer; composer = nullptr; } void ComposerTest::fillComposerData(Composer *composer) { composer->globalPart()->setFallbackCharsetEnabled(true); composer->infoPart()->setFrom(QStringLiteral("me@me.me")); composer->infoPart()->setTo(QStringList(QStringLiteral("you@you.you"))); composer->textPart()->setWrappedPlainText(QStringLiteral("All happy families are alike; each unhappy family is unhappy in its own way.")); } diff --git a/messagecomposer/autotests/cryptofunctions.cpp b/messagecomposer/autotests/cryptofunctions.cpp index c2b7adbb..5c6e14cd 100644 --- a/messagecomposer/autotests/cryptofunctions.cpp +++ b/messagecomposer/autotests/cryptofunctions.cpp @@ -1,229 +1,229 @@ /* Copyright (C) 2009 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.net Copyright (c) 2009 Leo Franchi 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 "cryptofunctions.h" #include "setupenv.h" #include #include #include #include #include #include #include #include #include #include using namespace MessageComposer; void ComposerTestUtil::verify(bool sign, bool encrypt, KMime::Content *content, const QByteArray &origContent, Kleo::CryptoMessageFormat f, KMime::Headers::contentEncoding encoding) { if (sign && encrypt) { verifySignatureAndEncryption(content, origContent, f, false); } else if (sign) { verifySignature(content, origContent, f, encoding); } else { verifyEncryption(content, origContent, f); } } void ComposerTestUtil::verifySignature(KMime::Content *content, const QByteArray &signedContent, Kleo::CryptoMessageFormat f, KMime::Headers::contentEncoding encoding) { // store it in a KMime::Message, that's what OTP needs KMime::Message *resultMessage = new KMime::Message; resultMessage->setContent(content->encodedContent()); resultMessage->parse(); // parse the result and make sure it is valid in various ways MimeTreeParser::SimpleObjectTreeSource testSource; MimeTreeParser::NodeHelper *nh = new MimeTreeParser::NodeHelper; MimeTreeParser::ObjectTreeParser otp(&testSource, nh); // ensure the signed part exists and is parseable if (f & Kleo::OpenPGPMIMEFormat) { // process the result.. otp.parseObjectTree(resultMessage); KMime::Content *signedPart = Util::findTypeInMessage(resultMessage, "application", "pgp-signature"); Q_ASSERT(signedPart); QCOMPARE(signedPart->contentTransferEncoding()->encoding(), KMime::Headers::CE7Bit); Q_UNUSED(signedPart); //Q_ASSERT( nh->signatureState( resultMessage ) == MimeTreeParser::KMMsgFullySigned ); QCOMPARE(MessageCore::NodeHelper::firstChild(resultMessage)->contentTransferEncoding()->encoding(), encoding); } else if (f & Kleo::InlineOpenPGPFormat) { // process the result.. otp.parseObjectTree(resultMessage); QCOMPARE(nh->signatureState(resultMessage), MimeTreeParser::KMMsgFullySigned); QCOMPARE(resultMessage->contentTransferEncoding()->encoding(), encoding); } else if (f & Kleo::AnySMIME) { if (f & Kleo::SMIMEFormat) { KMime::Content *signedPart = Util::findTypeInMessage(resultMessage, "application", "pkcs7-signature"); Q_ASSERT(signedPart); QCOMPARE(signedPart->contentTransferEncoding()->encoding(), KMime::Headers::CEbase64); QCOMPARE(signedPart->contentType()->mimeType(), QByteArray("application/pkcs7-signature")); - QCOMPARE(signedPart->contentType()->name(), QString::fromLatin1("smime.p7s")); + QCOMPARE(signedPart->contentType()->name(), QStringLiteral("smime.p7s")); QCOMPARE(signedPart->contentDisposition()->disposition(), KMime::Headers::CDattachment); - QCOMPARE(signedPart->contentDisposition()->filename(), QString::fromLatin1("smime.p7s")); + QCOMPARE(signedPart->contentDisposition()->filename(), QStringLiteral("smime.p7s")); Q_UNUSED(signedPart); QCOMPARE(MessageCore::NodeHelper::firstChild(resultMessage)->contentTransferEncoding()->encoding(), encoding); QCOMPARE(resultMessage->contentType()->mimeType(), QByteArray("multipart/signed")); - QCOMPARE(resultMessage->contentType()->parameter(QString::fromLatin1("protocol")), QString::fromLatin1("application/pkcs7-signature")); - QCOMPARE(resultMessage->contentType()->parameter(QString::fromLatin1("micalg")), QString::fromLatin1("sha1")); + QCOMPARE(resultMessage->contentType()->parameter(QStringLiteral("protocol")), QStringLiteral("application/pkcs7-signature")); + QCOMPARE(resultMessage->contentType()->parameter(QStringLiteral("micalg")), QStringLiteral("sha1")); } else if (f & Kleo::SMIMEOpaqueFormat) { KMime::Content *signedPart = Util::findTypeInMessage(resultMessage, "application", "pkcs7-mime"); Q_ASSERT(signedPart); QCOMPARE(signedPart->contentTransferEncoding()->encoding(), KMime::Headers::CEbase64); QCOMPARE(signedPart->contentType()->mimeType(), QByteArray("application/pkcs7-mime")); - QCOMPARE(signedPart->contentType()->name(), QString::fromLatin1("smime.p7m")); - QCOMPARE(signedPart->contentType()->parameter(QString::fromLatin1("smime-type")), QString::fromLatin1("signed-data")); + QCOMPARE(signedPart->contentType()->name(), QStringLiteral("smime.p7m")); + QCOMPARE(signedPart->contentType()->parameter(QStringLiteral("smime-type")), QStringLiteral("signed-data")); QCOMPARE(signedPart->contentDisposition()->disposition(), KMime::Headers::CDattachment); - QCOMPARE(signedPart->contentDisposition()->filename(), QString::fromLatin1("smime.p7m")); + QCOMPARE(signedPart->contentDisposition()->filename(), QStringLiteral("smime.p7m")); Q_UNUSED(signedPart); } // process the result.. otp.parseObjectTree(resultMessage); //Q_ASSERT( nh->signatureState( resultMessage ) == MimeTreeParser::KMMsgFullySigned ); } // make sure the good sig is of what we think it is QCOMPARE(otp.plainTextContent().trimmed(), QString::fromUtf8(signedContent).trimmed()); Q_UNUSED(signedContent); } void ComposerTestUtil::verifyEncryption(KMime::Content *content, const QByteArray &encrContent, Kleo::CryptoMessageFormat f, bool withAttachment) { // store it in a KMime::Message, that's what OTP needs KMime::Message::Ptr resultMessage(new KMime::Message); resultMessage->setContent(content->encodedContent()); resultMessage->parse(); // parse the result and make sure it is valid in various ways MimeTreeParser::SimpleObjectTreeSource testSource; testSource.setDecryptMessage(true); MimeTreeParser::NodeHelper *nh = new MimeTreeParser::NodeHelper; MimeTreeParser::ObjectTreeParser otp(&testSource, nh); if (f & Kleo::OpenPGPMIMEFormat) { // ensure the enc part exists and is parseable KMime::Content *encPart = Util::findTypeInMessage(resultMessage.data(), "application", "pgp-encrypted"); Q_ASSERT(encPart); Q_UNUSED(encPart); // process the result.. otp.parseObjectTree(resultMessage.data()); QCOMPARE(nh->encryptionState(resultMessage.data()), MimeTreeParser::KMMsgFullyEncrypted); } else if (f & Kleo::InlineOpenPGPFormat) { if (withAttachment) { //Only first MimePart is the encrypted Text KMime::Content *cContent = MessageCore::NodeHelper::firstChild(resultMessage.data()); resultMessage->setContent(cContent->encodedContent()); resultMessage->parse(); } otp.parseObjectTree(resultMessage.data()); QCOMPARE(nh->encryptionState(resultMessage.data()), MimeTreeParser::KMMsgFullyEncrypted); } else if (f & Kleo::AnySMIME) { // ensure the enc part exists and is parseable KMime::Content *encPart = Util::findTypeInMessage(resultMessage.data(), "application", "pkcs7-mime"); Q_ASSERT(encPart); QCOMPARE(encPart->contentType()->mimeType(), QByteArray("application/pkcs7-mime")); - QCOMPARE(encPart->contentType()->name(), QString::fromLatin1("smime.p7m")); - QCOMPARE(encPart->contentType()->parameter(QString::fromLatin1("smime-type")), QString::fromLatin1("enveloped-data")); + QCOMPARE(encPart->contentType()->name(), QStringLiteral("smime.p7m")); + QCOMPARE(encPart->contentType()->parameter(QStringLiteral("smime-type")), QStringLiteral("enveloped-data")); QCOMPARE(encPart->contentDisposition()->disposition(), KMime::Headers::CDattachment); - QCOMPARE(encPart->contentDisposition()->filename(), QString::fromLatin1("smime.p7m")); + QCOMPARE(encPart->contentDisposition()->filename(), QStringLiteral("smime.p7m")); Q_UNUSED(encPart); otp.parseObjectTree(resultMessage.data()); QCOMPARE(nh->encryptionState(resultMessage.data()), MimeTreeParser::KMMsgFullyEncrypted); } QCOMPARE(otp.plainTextContent().trimmed(), QString::fromUtf8(encrContent).trimmed()); Q_UNUSED(encrContent); } void ComposerTestUtil::verifySignatureAndEncryption(KMime::Content *content, const QByteArray &origContent, Kleo::CryptoMessageFormat f, bool withAttachment, bool combind) { Q_UNUSED(withAttachment); // store it in a KMime::Message, that's what OTP needs KMime::Message::Ptr resultMessage(new KMime::Message); resultMessage->setContent(content->encodedContent()); resultMessage->parse(); // parse the result and make sure it is valid in various ways MimeTreeParser::SimpleObjectTreeSource testSource; testSource.setDecryptMessage(true); MimeTreeParser::NodeHelper *nh = new MimeTreeParser::NodeHelper; MimeTreeParser::ObjectTreeParser otp(&testSource, nh); if (f & Kleo::OpenPGPMIMEFormat) { // ensure the enc part exists and is parseable KMime::Content *encPart = Util::findTypeInMessage(resultMessage.data(), "application", "pgp-encrypted"); Q_ASSERT(encPart); Q_UNUSED(encPart); otp.parseObjectTree(resultMessage.data()); QCOMPARE(nh->encryptionState(resultMessage.data()), MimeTreeParser::KMMsgFullyEncrypted); QVector< KMime::Content * > extra = nh->extraContents(resultMessage.data()); QCOMPARE(extra.size(), 1); if (combind) { QCOMPARE(nh->signatureState(resultMessage.data()), MimeTreeParser::KMMsgFullySigned); } else { QVector< KMime::Content * > extra = nh->extraContents(resultMessage.data()); QCOMPARE(extra.size(), 1); QCOMPARE(nh->signatureState(extra[ 0 ]), MimeTreeParser::KMMsgFullySigned); } } else if (f & Kleo::InlineOpenPGPFormat) { otp.parseObjectTree(resultMessage.data()); QCOMPARE(nh->encryptionState(resultMessage.data()), MimeTreeParser::KMMsgFullyEncrypted); QCOMPARE(nh->signatureState(resultMessage.data()), MimeTreeParser::KMMsgFullySigned); } else if (f & Kleo::AnySMIME) { KMime::Content *encPart = Util::findTypeInMessage(resultMessage.data(), "application", "pkcs7-mime"); Q_ASSERT(encPart); QCOMPARE(encPart->contentType()->mimeType(), QByteArray("application/pkcs7-mime")); - QCOMPARE(encPart->contentType()->name(), QString::fromLatin1("smime.p7m")); - QCOMPARE(encPart->contentType()->parameter(QString::fromLatin1("smime-type")), QString::fromLatin1("enveloped-data")); + QCOMPARE(encPart->contentType()->name(), QStringLiteral("smime.p7m")); + QCOMPARE(encPart->contentType()->parameter(QStringLiteral("smime-type")), QStringLiteral("enveloped-data")); QCOMPARE(encPart->contentDisposition()->disposition(), KMime::Headers::CDattachment); - QCOMPARE(encPart->contentDisposition()->filename(), QString::fromLatin1("smime.p7m")); + QCOMPARE(encPart->contentDisposition()->filename(), QStringLiteral("smime.p7m")); Q_UNUSED(encPart); otp.parseObjectTree(resultMessage.data()); QCOMPARE(nh->encryptionState(resultMessage.data()), MimeTreeParser::KMMsgFullyEncrypted); QVector< KMime::Content * > extra = nh->extraContents(resultMessage.data()); QCOMPARE(extra.size(), 1); QCOMPARE(nh->signatureState(extra[ 0 ]), MimeTreeParser::KMMsgFullySigned); } QCOMPARE(otp.plainTextContent().trimmed(), QString::fromUtf8(origContent).trimmed()); Q_UNUSED(origContent); } diff --git a/messagecomposer/src/attachment/attachmentcontrollerbase.cpp b/messagecomposer/src/attachment/attachmentcontrollerbase.cpp index 5cc5b0c1..bb39c9f9 100644 --- a/messagecomposer/src/attachment/attachmentcontrollerbase.cpp +++ b/messagecomposer/src/attachment/attachmentcontrollerbase.cpp @@ -1,1075 +1,1075 @@ /* * This file is part of KMail. * Copyright (c) 2009 Constantin Berzan * * Parts based on KMail code by: * Various authors. * * 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. */ #include "attachmentcontrollerbase.h" #include #include "MessageComposer/AttachmentJob" #include "MessageComposer/AttachmentFromPublicKeyJob" #include "MessageComposer/AttachmentVcardFromAddressBookJob" #include "MessageComposer/AttachmentClipBoardJob" #include "MessageComposer/Composer" #include "MessageComposer/GlobalPart" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "messagecomposer_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "attachment/attachmentfrommimecontentjob.h" #include "attachment/attachmentfromurljob.h" #include #include "messagecore/attachmentupdatejob.h" #include #include #include #include #if KIO_VERSION >= QT_VERSION_CHECK(5, 71, 0) #include #include #endif #include #include #include #include using namespace MessageComposer; using namespace MessageCore; class Q_DECL_HIDDEN MessageComposer::AttachmentControllerBase::Private { public: Private(AttachmentControllerBase *qq); ~Private(); void attachmentRemoved(const AttachmentPart::Ptr &part); // slot void compressJobResult(KJob *job); // slot void loadJobResult(KJob *job); // slot void openSelectedAttachments(); // slot void viewSelectedAttachments(); // slot void editSelectedAttachment(); // slot void editSelectedAttachmentWith(); // slot void removeSelectedAttachments(); // slot void saveSelectedAttachmentAs(); // slot void selectedAttachmentProperties(); // slot void editDone(MessageViewer::EditorWatcher *watcher); // slot void attachPublicKeyJobResult(KJob *job); // slot void slotAttachmentContentCreated(KJob *job); // slot void addAttachmentPart(AttachmentPart::Ptr part); void attachVcardFromAddressBook(KJob *job); //slot void attachClipBoardElement(KJob *job); void selectedAllAttachment(); void createOpenWithMenu(QMenu *topMenu, const AttachmentPart::Ptr &part); void reloadAttachment(); void updateJobResult(KJob *); AttachmentPart::List selectedParts; AttachmentControllerBase *const q; MessageComposer::AttachmentModel *model = nullptr; QWidget *wParent = nullptr; QHash editorPart; QHash editorTempFile; KActionCollection *mActionCollection = nullptr; QAction *attachPublicKeyAction = nullptr; QAction *attachMyPublicKeyAction = nullptr; QAction *openContextAction = nullptr; QAction *viewContextAction = nullptr; QAction *editContextAction = nullptr; QAction *editWithContextAction = nullptr; QAction *removeAction = nullptr; QAction *removeContextAction = nullptr; QAction *saveAsAction = nullptr; QAction *saveAsContextAction = nullptr; QAction *propertiesAction = nullptr; QAction *propertiesContextAction = nullptr; QAction *addAttachmentFileAction = nullptr; QAction *addAttachmentDirectoryAction = nullptr; QAction *addContextAction = nullptr; QAction *selectAllAction = nullptr; KActionMenu *attachmentMenu = nullptr; QAction *addOwnVcardAction = nullptr; QAction *reloadAttachmentAction = nullptr; QAction *attachVCardsAction = nullptr; QAction *attachClipBoardAction = nullptr; // If part p is compressed, uncompressedParts[p] is the uncompressed part. QHash uncompressedParts; bool encryptEnabled = false; bool signEnabled = false; }; AttachmentControllerBase::Private::Private(AttachmentControllerBase *qq) : q(qq) { } AttachmentControllerBase::Private::~Private() { } void AttachmentControllerBase::setSelectedParts(const AttachmentPart::List &selectedParts) { d->selectedParts = selectedParts; const int selectedCount = selectedParts.count(); const bool enableEditAction = (selectedCount == 1) && (!selectedParts.first()->isMessageOrMessageCollection()); d->openContextAction->setEnabled(selectedCount > 0); d->viewContextAction->setEnabled(selectedCount > 0); d->editContextAction->setEnabled(enableEditAction); d->editWithContextAction->setEnabled(enableEditAction); d->removeAction->setEnabled(selectedCount > 0); d->removeContextAction->setEnabled(selectedCount > 0); d->saveAsAction->setEnabled(selectedCount == 1); d->saveAsContextAction->setEnabled(selectedCount == 1); d->propertiesAction->setEnabled(selectedCount == 1); d->propertiesContextAction->setEnabled(selectedCount == 1); } void AttachmentControllerBase::Private::attachmentRemoved(const AttachmentPart::Ptr &part) { uncompressedParts.remove(part); } void AttachmentControllerBase::Private::compressJobResult(KJob *job) { if (job->error()) { KMessageBox::sorry(wParent, job->errorString(), i18n("Failed to compress attachment")); return; } Q_ASSERT(dynamic_cast(job)); AttachmentCompressJob *ajob = static_cast(job); AttachmentPart::Ptr originalPart = ajob->originalPart(); AttachmentPart::Ptr compressedPart = ajob->compressedPart(); if (ajob->isCompressedPartLarger()) { const int result = KMessageBox::questionYesNo(wParent, i18n("The compressed attachment is larger than the original. " "Do you want to keep the original one?"), QString(/*caption*/), KGuiItem(i18nc("Do not compress", "Keep")), KGuiItem(i18n("Compress"))); if (result == KMessageBox::Yes) { // The user has chosen to keep the uncompressed file. return; } } qCDebug(MESSAGECOMPOSER_LOG) << "Replacing uncompressed part in model."; uncompressedParts[ compressedPart ] = originalPart; bool ok = model->replaceAttachment(originalPart, compressedPart); if (!ok) { // The attachment was removed from the model while we were compressing. qCDebug(MESSAGECOMPOSER_LOG) << "Compressed a zombie."; } } void AttachmentControllerBase::Private::loadJobResult(KJob *job) { if (job->error()) { KMessageBox::sorry(wParent, job->errorString(), i18n("Failed to attach file")); return; } Q_ASSERT(dynamic_cast(job)); AttachmentLoadJob *ajob = static_cast(job); AttachmentPart::Ptr part = ajob->attachmentPart(); q->addAttachment(part); } void AttachmentControllerBase::Private::openSelectedAttachments() { Q_ASSERT(selectedParts.count() >= 1); for (const AttachmentPart::Ptr &part : qAsConst(selectedParts)) { q->openAttachment(part); } } void AttachmentControllerBase::Private::viewSelectedAttachments() { Q_ASSERT(selectedParts.count() >= 1); for (const AttachmentPart::Ptr &part : qAsConst(selectedParts)) { q->viewAttachment(part); } } void AttachmentControllerBase::Private::editSelectedAttachment() { Q_ASSERT(selectedParts.count() == 1); q->editAttachment(selectedParts.constFirst(), MessageViewer::EditorWatcher::NoOpenWithDialog); } void AttachmentControllerBase::Private::editSelectedAttachmentWith() { Q_ASSERT(selectedParts.count() == 1); q->editAttachment(selectedParts.first(), MessageViewer::EditorWatcher::OpenWithDialog); } void AttachmentControllerBase::Private::removeSelectedAttachments() { Q_ASSERT(selectedParts.count() >= 1); //We must store list, otherwise when we remove it changes selectedParts (as selection changed) => it will crash. const AttachmentPart::List toRemove = selectedParts; for (const AttachmentPart::Ptr &part : toRemove) { model->removeAttachment(part); } } void AttachmentControllerBase::Private::saveSelectedAttachmentAs() { Q_ASSERT(selectedParts.count() == 1); q->saveAttachmentAs(selectedParts.constFirst()); } void AttachmentControllerBase::Private::selectedAttachmentProperties() { Q_ASSERT(selectedParts.count() == 1); q->attachmentProperties(selectedParts.constFirst()); } void AttachmentControllerBase::Private::reloadAttachment() { Q_ASSERT(selectedParts.count() == 1); AttachmentUpdateJob *ajob = new AttachmentUpdateJob(selectedParts.constFirst(), q); connect(ajob, &AttachmentUpdateJob::result, q, [this](KJob *job) { updateJobResult(job); }); ajob->start(); } void AttachmentControllerBase::Private::updateJobResult(KJob *job) { if (job->error()) { KMessageBox::sorry(wParent, job->errorString(), i18n("Failed to reload attachment")); return; } Q_ASSERT(dynamic_cast(job)); AttachmentUpdateJob *ajob = static_cast(job); AttachmentPart::Ptr originalPart = ajob->originalPart(); AttachmentPart::Ptr updatedPart = ajob->updatedPart(); attachmentRemoved(originalPart); bool ok = model->replaceAttachment(originalPart, updatedPart); if (!ok) { // The attachment was removed from the model while we were compressing. qCDebug(MESSAGECOMPOSER_LOG) << "Updated a zombie."; } } void AttachmentControllerBase::Private::editDone(MessageViewer::EditorWatcher *watcher) { AttachmentPart::Ptr part = editorPart.take(watcher); Q_ASSERT(part); QTemporaryFile *tempFile = editorTempFile.take(watcher); Q_ASSERT(tempFile); if (watcher->fileChanged()) { qCDebug(MESSAGECOMPOSER_LOG) << "File has changed."; const QString name = watcher->url().path(); QFile file(name); if (file.open(QIODevice::ReadOnly)) { const QByteArray data = file.readAll(); part->setData(data); model->updateAttachment(part); } } delete tempFile; // The watcher deletes itself. } void AttachmentControllerBase::Private::createOpenWithMenu(QMenu *topMenu, const AttachmentPart::Ptr &part) { const QString contentTypeStr = QString::fromLatin1(part->mimeType()); const KService::List offers = KFileItemActions::associatedApplications(QStringList() << contentTypeStr, QString()); if (!offers.isEmpty()) { QMenu *menu = topMenu; QActionGroup *actionGroup = new QActionGroup(menu); connect(actionGroup, &QActionGroup::triggered, q, &AttachmentControllerBase::slotOpenWithAction); if (offers.count() > 1) { // submenu 'open with' menu = new QMenu(i18nc("@title:menu", "&Open With"), topMenu); menu->menuAction()->setObjectName(QStringLiteral("openWith_submenu")); // for the unittest topMenu->addMenu(menu); } //qCDebug(MESSAGECOMPOSER_LOG) << offers.count() << "offers" << topMenu << menu; KService::List::ConstIterator it = offers.constBegin(); KService::List::ConstIterator end = offers.constEnd(); for (; it != end; ++it) { QAction *act = MessageViewer::Util::createAppAction(*it, // no submenu -> prefix single offer menu == topMenu, actionGroup, menu); menu->addAction(act); } QString openWithActionName; if (menu != topMenu) { // submenu menu->addSeparator(); openWithActionName = i18nc("@action:inmenu Open With", "&Other..."); } else { openWithActionName = i18nc("@title:menu", "&Open With..."); } QAction *openWithAct = new QAction(menu); openWithAct->setText(openWithActionName); QObject::connect(openWithAct, &QAction::triggered, q, &AttachmentControllerBase::slotOpenWithDialog); menu->addAction(openWithAct); } else { // no app offers -> Open With... QAction *act = new QAction(topMenu); act->setText(i18nc("@title:menu", "&Open With...")); QObject::connect(act, &QAction::triggered, q, &AttachmentControllerBase::slotOpenWithDialog); topMenu->addAction(act); } } void AttachmentControllerBase::exportPublicKey(const QString &fingerprint) { if (fingerprint.isEmpty() || !QGpgME::openpgp()) { qCWarning(MESSAGECOMPOSER_LOG) << "Tried to export key with empty fingerprint, or no OpenPGP."; return; } MessageComposer::AttachmentFromPublicKeyJob *ajob = new MessageComposer::AttachmentFromPublicKeyJob(fingerprint, this); connect(ajob, &AttachmentFromPublicKeyJob::result, this, [this](KJob *job) { d->attachPublicKeyJobResult(job); }); ajob->start(); } void AttachmentControllerBase::Private::attachPublicKeyJobResult(KJob *job) { // The only reason we can't use loadJobResult() and need a separate method // is that we want to show the proper caption ("public key" instead of "file")... if (job->error()) { KMessageBox::sorry(wParent, job->errorString(), i18n("Failed to attach public key")); return; } Q_ASSERT(dynamic_cast(job)); MessageComposer::AttachmentFromPublicKeyJob *ajob = static_cast(job); AttachmentPart::Ptr part = ajob->attachmentPart(); q->addAttachment(part); } void AttachmentControllerBase::Private::attachVcardFromAddressBook(KJob *job) { if (job->error()) { qCDebug(MESSAGECOMPOSER_LOG) << " Error during when get vCard"; KMessageBox::sorry(wParent, job->errorString(), i18n("Failed to attach vCard")); return; } MessageComposer::AttachmentVcardFromAddressBookJob *ajob = static_cast(job); AttachmentPart::Ptr part = ajob->attachmentPart(); q->addAttachment(part); } void AttachmentControllerBase::Private::attachClipBoardElement(KJob *job) { if (job->error()) { qCDebug(MESSAGECOMPOSER_LOG) << " Error during when get try to attach text from clipboard"; KMessageBox::sorry(wParent, job->errorString(), i18n("Failed to attach text from clipboard")); return; } MessageComposer::AttachmentClipBoardJob *ajob = static_cast(job); AttachmentPart::Ptr part = ajob->attachmentPart(); q->addAttachment(part); } static QTemporaryFile *dumpAttachmentToTempFile(const AttachmentPart::Ptr &part) // local { QTemporaryFile *file = new QTemporaryFile; if (!file->open()) { qCCritical(MESSAGECOMPOSER_LOG) << "Could not open tempfile" << file->fileName(); delete file; return nullptr; } if (file->write(part->data()) == -1) { qCCritical(MESSAGECOMPOSER_LOG) << "Could not dump attachment to tempfile."; delete file; return nullptr; } file->flush(); return file; } AttachmentControllerBase::AttachmentControllerBase(MessageComposer::AttachmentModel *model, QWidget *wParent, KActionCollection *actionCollection) : QObject(wParent) , d(new Private(this)) { d->model = model; connect(model, &MessageComposer::AttachmentModel::attachUrlsRequested, this, &AttachmentControllerBase::addAttachments); connect(model, &MessageComposer::AttachmentModel::attachmentRemoved, this, [this](const MessageCore::AttachmentPart::Ptr &attr) { d->attachmentRemoved(attr); }); connect(model, &AttachmentModel::attachmentCompressRequested, this, &AttachmentControllerBase::compressAttachment); connect(model, &MessageComposer::AttachmentModel::encryptEnabled, this, &AttachmentControllerBase::setEncryptEnabled); connect(model, &MessageComposer::AttachmentModel::signEnabled, this, &AttachmentControllerBase::setSignEnabled); d->wParent = wParent; d->mActionCollection = actionCollection; } AttachmentControllerBase::~AttachmentControllerBase() { delete d; } void AttachmentControllerBase::createActions() { // Create the actions. d->attachPublicKeyAction = new QAction(i18n("Attach &Public Key..."), this); connect(d->attachPublicKeyAction, &QAction::triggered, this, &AttachmentControllerBase::showAttachPublicKeyDialog); d->attachMyPublicKeyAction = new QAction(i18n("Attach &My Public Key"), this); connect(d->attachMyPublicKeyAction, &QAction::triggered, this, &AttachmentControllerBase::attachMyPublicKey); d->attachmentMenu = new KActionMenu(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18n("Attach"), this); connect(d->attachmentMenu, &QAction::triggered, this, &AttachmentControllerBase::showAddAttachmentFileDialog); d->attachmentMenu->setDelayed(true); d->addAttachmentFileAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18n("&Attach File..."), this); d->addAttachmentFileAction->setIconText(i18n("Attach")); d->addContextAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18n("Add Attachment..."), this); connect(d->addAttachmentFileAction, &QAction::triggered, this, &AttachmentControllerBase::showAddAttachmentFileDialog); connect(d->addContextAction, &QAction::triggered, this, &AttachmentControllerBase::showAddAttachmentFileDialog); d->addAttachmentDirectoryAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18n("&Attach Directory..."), this); d->addAttachmentDirectoryAction->setIconText(i18n("Attach")); connect(d->addAttachmentDirectoryAction, &QAction::triggered, this, &AttachmentControllerBase::showAddAttachmentCompressedDirectoryDialog); d->addOwnVcardAction = new QAction(i18n("Attach Own vCard"), this); d->addOwnVcardAction->setIconText(i18n("Own vCard")); d->addOwnVcardAction->setCheckable(true); connect(d->addOwnVcardAction, &QAction::triggered, this, &AttachmentControllerBase::addOwnVcard); d->attachVCardsAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18n("&Attach vCards..."), this); d->attachVCardsAction->setIconText(i18n("Attach")); connect(d->attachVCardsAction, &QAction::triggered, this, &AttachmentControllerBase::showAttachVcard); d->attachClipBoardAction = new QAction(QIcon::fromTheme(QStringLiteral("mail-attachment")), i18n("&Attach Text From Clipboard..."), this); d->attachClipBoardAction->setIconText(i18n("Attach Text From Clipboard")); connect(d->attachClipBoardAction, &QAction::triggered, this, &AttachmentControllerBase::showAttachClipBoard); d->attachmentMenu->addAction(d->addAttachmentFileAction); d->attachmentMenu->addAction(d->addAttachmentDirectoryAction); d->attachmentMenu->addSeparator(); d->attachmentMenu->addAction(d->addOwnVcardAction); d->attachmentMenu->addSeparator(); d->attachmentMenu->addAction(d->attachVCardsAction); d->attachmentMenu->addSeparator(); d->attachmentMenu->addAction(d->attachClipBoardAction); d->removeAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("&Remove Attachment"), this); d->removeContextAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Remove"), this); // FIXME need two texts. is there a better way? connect(d->removeAction, &QAction::triggered, this, [this]() { d->removeSelectedAttachments(); }); connect(d->removeContextAction, &QAction::triggered, this, [this]() { d->removeSelectedAttachments(); }); d->openContextAction = new QAction(i18nc("to open", "Open"), this); connect(d->openContextAction, &QAction::triggered, this, [this]() { d->openSelectedAttachments(); }); d->viewContextAction = new QAction(i18nc("to view", "View"), this); connect(d->viewContextAction, &QAction::triggered, this, [this]() { d->viewSelectedAttachments(); }); d->editContextAction = new QAction(i18nc("to edit", "Edit"), this); connect(d->editContextAction, &QAction::triggered, this, [this]() { d->editSelectedAttachment(); }); d->editWithContextAction = new QAction(i18n("Edit With..."), this); connect(d->editWithContextAction, &QAction::triggered, this, [this]() { d->editSelectedAttachmentWith(); }); d->saveAsAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("&Save Attachment As..."), this); d->saveAsContextAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("Save As..."), this); connect(d->saveAsAction, &QAction::triggered, this, [this]() { d->saveSelectedAttachmentAs(); }); connect(d->saveAsContextAction, &QAction::triggered, this, [this]() { d->saveSelectedAttachmentAs(); }); d->propertiesAction = new QAction(i18n("Attachment Pr&operties..."), this); d->propertiesContextAction = new QAction(i18n("Properties"), this); connect(d->propertiesAction, &QAction::triggered, this, [this]() { d->selectedAttachmentProperties(); }); connect(d->propertiesContextAction, &QAction::triggered, this, [this]() { d->selectedAttachmentProperties(); }); d->selectAllAction = new QAction(i18n("Select All"), this); connect(d->selectAllAction, &QAction::triggered, this, &AttachmentControllerBase::selectedAllAttachment); d->reloadAttachmentAction = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18n("Reload"), this); connect(d->reloadAttachmentAction, &QAction::triggered, this, [this]() { d->reloadAttachment(); }); // Insert the actions into the composer window's menu. KActionCollection *collection = d->mActionCollection; collection->addAction(QStringLiteral("attach_public_key"), d->attachPublicKeyAction); collection->addAction(QStringLiteral("attach_my_public_key"), d->attachMyPublicKeyAction); collection->addAction(QStringLiteral("attach"), d->addAttachmentFileAction); collection->addAction(QStringLiteral("attach_directory"), d->addAttachmentDirectoryAction); collection->addAction(QStringLiteral("remove"), d->removeAction); collection->addAction(QStringLiteral("attach_save"), d->saveAsAction); collection->addAction(QStringLiteral("attach_properties"), d->propertiesAction); collection->addAction(QStringLiteral("select_all_attachment"), d->selectAllAction); collection->addAction(QStringLiteral("attach_menu"), d->attachmentMenu); collection->addAction(QStringLiteral("attach_own_vcard"), d->addOwnVcardAction); collection->addAction(QStringLiteral("attach_vcards"), d->attachVCardsAction); setSelectedParts(AttachmentPart::List()); Q_EMIT actionsCreated(); } void AttachmentControllerBase::setEncryptEnabled(bool enabled) { d->encryptEnabled = enabled; } void AttachmentControllerBase::setSignEnabled(bool enabled) { d->signEnabled = enabled; } void AttachmentControllerBase::compressAttachment(const AttachmentPart::Ptr &part, bool compress) { if (compress) { qCDebug(MESSAGECOMPOSER_LOG) << "Compressing part."; AttachmentCompressJob *ajob = new AttachmentCompressJob(part, this); connect(ajob, &AttachmentCompressJob::result, this, [this](KJob *job) { d->compressJobResult(job); }); ajob->start(); } else { qCDebug(MESSAGECOMPOSER_LOG) << "Uncompressing part."; // Replace the compressed part with the original uncompressed part, and delete // the compressed part. AttachmentPart::Ptr originalPart = d->uncompressedParts.take(part); Q_ASSERT(originalPart); // Found in uncompressedParts. bool ok = d->model->replaceAttachment(part, originalPart); Q_ASSERT(ok); Q_UNUSED(ok); } } void AttachmentControllerBase::showContextMenu() { Q_EMIT refreshSelection(); const int numberOfParts(d->selectedParts.count()); QMenu menu; const bool enableEditAction = (numberOfParts == 1) && (!d->selectedParts.first()->isMessageOrMessageCollection()); if (numberOfParts > 0) { if (numberOfParts == 1) { const QString mimetype = QString::fromLatin1(d->selectedParts.first()->mimeType()); QMimeDatabase mimeDb; auto mime = mimeDb.mimeTypeForName(mimetype); QStringList parentMimeType; if (mime.isValid()) { parentMimeType = mime.allAncestors(); } if ((mimetype == QLatin1String("text/plain")) || (mimetype == QLatin1String("image/png")) || (mimetype == QLatin1String("image/jpeg")) || parentMimeType.contains(QLatin1String("text/plain")) || parentMimeType.contains(QLatin1String("image/png")) || parentMimeType.contains(QLatin1String("image/jpeg")) ) { menu.addAction(d->viewContextAction); } d->createOpenWithMenu(&menu, d->selectedParts.constFirst()); } menu.addAction(d->openContextAction); } if (enableEditAction) { menu.addAction(d->editWithContextAction); menu.addAction(d->editContextAction); } menu.addSeparator(); if (numberOfParts == 1) { if (!d->selectedParts.first()->url().isEmpty()) { menu.addAction(d->reloadAttachmentAction); } menu.addAction(d->saveAsContextAction); menu.addSeparator(); menu.addAction(d->propertiesContextAction); menu.addSeparator(); } if (numberOfParts > 0) { menu.addAction(d->removeContextAction); menu.addSeparator(); } const int nbAttachment = d->model->rowCount(); if (nbAttachment != numberOfParts) { menu.addAction(d->selectAllAction); menu.addSeparator(); } if (numberOfParts == 0) { menu.addAction(d->addContextAction); } menu.exec(QCursor::pos()); } void AttachmentControllerBase::slotOpenWithDialog() { openWith(); } void AttachmentControllerBase::slotOpenWithAction(QAction *act) { KService::Ptr app = act->data().value(); Q_ASSERT(d->selectedParts.count() == 1); openWith(app); } void AttachmentControllerBase::openWith(const KService::Ptr &offer) { - QTemporaryFile *tempFile = dumpAttachmentToTempFile(d->selectedParts.first()); + QTemporaryFile *tempFile = dumpAttachmentToTempFile(d->selectedParts.constFirst()); if (!tempFile) { KMessageBox::sorry(d->wParent, i18n("KMail was unable to write the attachment to a temporary file."), i18n("Unable to open attachment")); return; } QList lst; QUrl url = QUrl::fromLocalFile(tempFile->fileName()); lst.append(url); tempFile->setPermissions(QFile::ReadUser); bool result = false; if (offer) { result = KRun::runService(*offer, lst, d->wParent, false); } else { result = KRun::displayOpenWithDialog(lst, d->wParent, false); } if (!result) { delete tempFile; tempFile = nullptr; } else { // The file was opened. Delete it only when the composer is closed // (and this object is destroyed). tempFile->setParent(this); // Manages lifetime. } } void AttachmentControllerBase::openAttachment(const AttachmentPart::Ptr &part) { QTemporaryFile *tempFile = dumpAttachmentToTempFile(part); if (!tempFile) { KMessageBox::sorry(d->wParent, i18n("KMail was unable to write the attachment to a temporary file."), i18n("Unable to open attachment")); return; } tempFile->setPermissions(QFile::ReadUser); #if KIO_VERSION < QT_VERSION_CHECK(5, 71, 0) KRun::RunFlags flags; flags |= KRun::DeleteTemporaryFiles; bool success = KRun::runUrl(QUrl::fromLocalFile(tempFile->fileName()), QString::fromLatin1(part->mimeType()), d->wParent, flags); if (!success) { if (!KMimeTypeTrader::self()->preferredService(QString::fromLatin1(part->mimeType())).data()) { // KRun showed an Open-With dialog, and it was canceled. } else { // KRun failed. KMessageBox::sorry(d->wParent, i18n("KMail was unable to open the attachment."), i18n("Unable to open attachment")); } delete tempFile; tempFile = nullptr; } else { // The file was opened. Delete it only when the composer is closed // (and this object is destroyed). tempFile->setParent(this); // Manages lifetime. } #else KIO::OpenUrlJob *job = new KIO::OpenUrlJob(QUrl::fromLocalFile(tempFile->fileName()), QString::fromLatin1(part->mimeType())); job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, d->wParent)); job->setDeleteTemporaryFile(true); connect(job, &KIO::OpenUrlJob::result, this, [this, tempFile](KJob *job) { if (job->error() == KIO::ERR_USER_CANCELED) { KMessageBox::sorry(d->wParent, i18n("KMail was unable to open the attachment."), job->errorString()); delete tempFile; } else { // The file was opened. Delete it only when the composer is closed // (and this object is destroyed). tempFile->setParent(this); // Manages lifetime. } }); job->start(); #endif } void AttachmentControllerBase::viewAttachment(const AttachmentPart::Ptr &part) { MessageComposer::Composer *composer = new MessageComposer::Composer; composer->globalPart()->setFallbackCharsetEnabled(true); MessageComposer::AttachmentJob *attachmentJob = new MessageComposer::AttachmentJob(part, composer); connect(attachmentJob, &AttachmentJob::result, this, [this](KJob *job) { d->slotAttachmentContentCreated(job); }); attachmentJob->start(); } void AttachmentControllerBase::Private::slotAttachmentContentCreated(KJob *job) { if (!job->error()) { const MessageComposer::AttachmentJob *const attachmentJob = dynamic_cast(job); Q_ASSERT(attachmentJob); if (attachmentJob) { Q_EMIT q->showAttachment(attachmentJob->content(), QByteArray()); } } else { // TODO: show warning to the user qCWarning(MESSAGECOMPOSER_LOG) << "Error creating KMime::Content for attachment:" << job->errorText(); } } void AttachmentControllerBase::editAttachment(AttachmentPart::Ptr part, MessageViewer::EditorWatcher::OpenWithOption openWithOption) { QTemporaryFile *tempFile = dumpAttachmentToTempFile(part); if (!tempFile) { KMessageBox::sorry(d->wParent, i18n("KMail was unable to write the attachment to a temporary file."), i18n("Unable to edit attachment")); return; } MessageViewer::EditorWatcher *watcher = new MessageViewer::EditorWatcher( QUrl::fromLocalFile(tempFile->fileName()), QString::fromLatin1(part->mimeType()), openWithOption, this, d->wParent); connect(watcher, &MessageViewer::EditorWatcher::editDone, this, [this](MessageViewer::EditorWatcher *watcher) { d->editDone(watcher); }); switch (watcher->start()) { case MessageViewer::EditorWatcher::NoError: // The attachment is being edited. // We will clean things up in editDone(). d->editorPart[ watcher ] = part; d->editorTempFile[ watcher ] = tempFile; // Delete the temp file if the composer is closed (and this object is destroyed). tempFile->setParent(this); // Manages lifetime. break; case MessageViewer::EditorWatcher::CannotStart: qCWarning(MESSAGECOMPOSER_LOG) << "Could not start EditorWatcher."; Q_FALLTHROUGH(); case MessageViewer::EditorWatcher::Unknown: case MessageViewer::EditorWatcher::Canceled: case MessageViewer::EditorWatcher::NoServiceFound: delete watcher; delete tempFile; break; } } void AttachmentControllerBase::editAttachmentWith(const AttachmentPart::Ptr &part) { editAttachment(part, MessageViewer::EditorWatcher::OpenWithDialog); } void AttachmentControllerBase::saveAttachmentAs(const AttachmentPart::Ptr &part) { QString pname = part->name(); if (pname.isEmpty()) { pname = i18n("unnamed"); } QUrl url = QFileDialog::getSaveFileUrl(d->wParent, i18n("Save Attachment As"), QUrl::fromLocalFile(pname)); if (url.isEmpty()) { qCDebug(MESSAGECOMPOSER_LOG) << "Save Attachment As dialog canceled."; return; } byteArrayToRemoteFile(part->data(), url); } void AttachmentControllerBase::byteArrayToRemoteFile(const QByteArray &aData, const QUrl &aURL, bool overwrite) { KIO::StoredTransferJob *job = KIO::storedPut(aData, aURL, -1, overwrite ? KIO::Overwrite : KIO::DefaultFlags); connect(job, &KIO::StoredTransferJob::result, this, &AttachmentControllerBase::slotPutResult); } void AttachmentControllerBase::slotPutResult(KJob *job) { KIO::StoredTransferJob *_job = qobject_cast(job); if (job->error()) { if (job->error() == KIO::ERR_FILE_ALREADY_EXIST) { if (KMessageBox::warningContinueCancel(nullptr, i18n("File %1 exists.\nDo you want to replace it?", _job->url().toLocalFile()), i18n("Save to File"), KGuiItem(i18n("&Replace"))) == KMessageBox::Continue) { byteArrayToRemoteFile(_job->data(), _job->url(), true); } } else { KJobUiDelegate *ui = static_cast(job)->uiDelegate(); ui->showErrorMessage(); } } } void AttachmentControllerBase::attachmentProperties(const AttachmentPart::Ptr &part) { QPointer dialog = new AttachmentPropertiesDialog( part, false, d->wParent); dialog->setEncryptEnabled(d->encryptEnabled); dialog->setSignEnabled(d->signEnabled); if (dialog->exec() && dialog) { d->model->updateAttachment(part); } delete dialog; } void AttachmentControllerBase::attachDirectory(const QUrl &url) { const int rc = KMessageBox::warningYesNo(d->wParent, i18n("Do you really want to attach this directory \"%1\"?", url.toLocalFile()), i18nc("@title:window", "Attach directory")); if (rc == KMessageBox::Yes) { addAttachment(url); } } void AttachmentControllerBase::showAttachVcard() { QPointer dlg = new Akonadi::EmailAddressSelectionDialog(d->wParent); dlg->view()->view()->setSelectionMode(QAbstractItemView::MultiSelection); if (dlg->exec()) { const Akonadi::EmailAddressSelection::List selectedEmail = dlg->selectedAddresses(); for (const Akonadi::EmailAddressSelection &selected : selectedEmail) { MessageComposer::AttachmentVcardFromAddressBookJob *ajob = new MessageComposer::AttachmentVcardFromAddressBookJob(selected.item(), this); connect(ajob, &AttachmentVcardFromAddressBookJob::result, this, [this](KJob *job) { d->attachVcardFromAddressBook(job); }); ajob->start(); } } delete dlg; } void AttachmentControllerBase::showAttachClipBoard() { MessageComposer::AttachmentClipBoardJob *job = new MessageComposer::AttachmentClipBoardJob(this); connect(job, &AttachmentClipBoardJob::result, this, [this](KJob *job) { d->attachClipBoardElement(job); }); job->start(); } void AttachmentControllerBase::showAddAttachmentCompressedDirectoryDialog() { const QUrl url = QFileDialog::getExistingDirectoryUrl(d->wParent, i18nc("@title:window", "Attach Directory")); if (url.isValid()) { attachDirectory(url); } } void AttachmentControllerBase::showAddAttachmentFileDialog() { KEncodingFileDialog::Result result = KEncodingFileDialog::getOpenUrlsAndEncoding(QString(), QUrl(), QString(), d->wParent, i18nc("@title:window", "Attach File")); if (!result.URLs.isEmpty()) { const QString encoding = MimeTreeParser::NodeHelper::fixEncoding(result.encoding); const int numberOfFiles(result.URLs.count()); for (int i = 0; i < numberOfFiles; ++i) { const QUrl url = result.URLs.at(i); QUrl urlWithEncoding = url; MessageCore::StringUtil::setEncodingFile(urlWithEncoding, encoding); QMimeDatabase mimeDb; auto mimeType = mimeDb.mimeTypeForUrl(urlWithEncoding); if (mimeType.name() == QLatin1String("inode/directory")) { const int rc = KMessageBox::warningYesNo(d->wParent, i18n("Do you really want to attach this directory \"%1\"?", url.toLocalFile()), i18nc("@title:window", "Attach directory")); if (rc == KMessageBox::Yes) { addAttachment(urlWithEncoding); } } else { addAttachment(urlWithEncoding); } } } } void AttachmentControllerBase::addAttachment(const AttachmentPart::Ptr &part) { part->setEncrypted(d->model->isEncryptSelected()); part->setSigned(d->model->isSignSelected()); d->model->addAttachment(part); Q_EMIT fileAttached(); } void AttachmentControllerBase::addAttachmentUrlSync(const QUrl &url) { MessageCore::AttachmentFromUrlBaseJob *ajob = MessageCore::AttachmentFromUrlUtils::createAttachmentJob(url, this); if (ajob->exec()) { AttachmentPart::Ptr part = ajob->attachmentPart(); addAttachment(part); } else { if (ajob->error()) { KMessageBox::sorry(d->wParent, ajob->errorString(), i18nc("@title:window", "Failed to attach file")); } } } void AttachmentControllerBase::addAttachment(const QUrl &url) { MessageCore::AttachmentFromUrlBaseJob *ajob = MessageCore::AttachmentFromUrlUtils::createAttachmentJob(url, this); connect(ajob, &AttachmentFromUrlBaseJob::result, this, [this](KJob *job) { d->loadJobResult(job); }); ajob->start(); } void AttachmentControllerBase::addAttachments(const QList &urls) { for (const QUrl &url : urls) { addAttachment(url); } } void AttachmentControllerBase::showAttachPublicKeyDialog() { using Kleo::KeySelectionDialog; QPointer dialog = new KeySelectionDialog( i18n("Attach Public OpenPGP Key"), i18n("Select the public key which should be attached."), std::vector(), KeySelectionDialog::PublicKeys | KeySelectionDialog::OpenPGPKeys, false /* no multi selection */, false /* no remember choice box */, d->wParent); if (dialog->exec() == QDialog::Accepted) { exportPublicKey(dialog->fingerprint()); } delete dialog; } void AttachmentControllerBase::attachMyPublicKey() { } void AttachmentControllerBase::enableAttachPublicKey(bool enable) { d->attachPublicKeyAction->setEnabled(enable); } void AttachmentControllerBase::enableAttachMyPublicKey(bool enable) { d->attachMyPublicKeyAction->setEnabled(enable); } void AttachmentControllerBase::setAttachOwnVcard(bool attachVcard) { d->addOwnVcardAction->setChecked(attachVcard); } bool AttachmentControllerBase::attachOwnVcard() const { return d->addOwnVcardAction->isChecked(); } void AttachmentControllerBase::setIdentityHasOwnVcard(bool state) { d->addOwnVcardAction->setEnabled(state); } #include "moc_attachmentcontrollerbase.cpp" diff --git a/messagecomposer/src/composer-ng/richtextcomposerng.cpp b/messagecomposer/src/composer-ng/richtextcomposerng.cpp index 5e4d547f..63cba30f 100644 --- a/messagecomposer/src/composer-ng/richtextcomposerng.cpp +++ b/messagecomposer/src/composer-ng/richtextcomposerng.cpp @@ -1,436 +1,436 @@ /* Copyright (C) 2015-2020 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "richtextcomposerng.h" #include #include #include "richtextcomposersignatures.h" #include #include #include "settings/messagecomposersettings.h" #include #include #include using namespace MessageComposer; class MessageComposer::RichTextComposerNgPrivate { public: RichTextComposerNgPrivate(RichTextComposerNg *q) : richtextComposer(q) { richTextComposerSignatures = new MessageComposer::RichTextComposerSignatures(richtextComposer, richtextComposer); } void fixHtmlFontSize(QString &cleanHtml); QString toCleanHtml() const; PimCommon::AutoCorrection *autoCorrection = nullptr; RichTextComposerNg *richtextComposer = nullptr; MessageComposer::RichTextComposerSignatures *richTextComposerSignatures = nullptr; }; RichTextComposerNg::RichTextComposerNg(QWidget *parent) : KPIMTextEdit::RichTextComposer(parent) , d(new MessageComposer::RichTextComposerNgPrivate(this)) { } RichTextComposerNg::~RichTextComposerNg() { delete d; } MessageComposer::RichTextComposerSignatures *RichTextComposerNg::composerSignature() const { return d->richTextComposerSignatures; } PimCommon::AutoCorrection *RichTextComposerNg::autocorrection() const { return d->autoCorrection; } void RichTextComposerNg::setAutocorrection(PimCommon::AutoCorrection *autocorrect) { d->autoCorrection = autocorrect; } void RichTextComposerNg::setAutocorrectionLanguage(const QString &lang) { if (d->autoCorrection) { d->autoCorrection->setLanguage(lang); } } static bool isSpecial(const QTextCharFormat &charFormat) { return charFormat.isFrameFormat() || charFormat.isImageFormat() || charFormat.isListFormat() || charFormat.isTableFormat() || charFormat.isTableCellFormat(); } bool RichTextComposerNg::processModifyText(QKeyEvent *e) { if (d->autoCorrection && d->autoCorrection->isEnabledAutoCorrection()) { if ((e->key() == Qt::Key_Space) || (e->key() == Qt::Key_Enter) || (e->key() == Qt::Key_Return)) { if (!isLineQuoted(textCursor().block().text()) && !textCursor().hasSelection()) { const QTextCharFormat initialTextFormat = textCursor().charFormat(); const bool richText = (textMode() == RichTextComposer::Rich); int position = textCursor().position(); const bool addSpace = d->autoCorrection->autocorrect(richText, *document(), position); QTextCursor cur = textCursor(); cur.setPosition(position); const bool spacePressed = (e->key() == Qt::Key_Space); if (overwriteMode() && spacePressed) { if (addSpace) { const QChar insertChar = QLatin1Char(' '); if (!cur.atBlockEnd()) { cur.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, 1); } if (richText && !isSpecial(initialTextFormat)) { cur.insertText(insertChar, initialTextFormat); } else { cur.insertText(insertChar); } setTextCursor(cur); } } else { const QChar insertChar = spacePressed ? QLatin1Char(' ') : QLatin1Char('\n'); if (richText && !isSpecial(initialTextFormat)) { if ((spacePressed && addSpace) || !spacePressed) { cur.insertText(insertChar, initialTextFormat); } } else { if ((spacePressed && addSpace) || !spacePressed) { cur.insertText(insertChar); } } setTextCursor(cur); } return true; } } } return false; } void RichTextComposerNgPrivate::fixHtmlFontSize(QString &cleanHtml) { static const QString FONTSTYLEREGEX = QStringLiteral(""); QRegExp styleRegex(FONTSTYLEREGEX); styleRegex.setMinimal(true); int offset = styleRegex.indexIn(cleanHtml, 0); while (offset != -1) { // replace all the matching text with the new line text bool ok = false; const QString fontSizeStr = styleRegex.cap(1); const int ptValue = fontSizeStr.toInt(&ok); if (ok) { double emValue = static_cast(ptValue) / 12; const QString emValueStr = QString::number(emValue, 'g', 2); cleanHtml.replace(styleRegex.pos(1), QString(fontSizeStr + QLatin1String("px")).length(), emValueStr + QLatin1String("em")); } // advance the search offset to just beyond the last replace offset += styleRegex.matchedLength(); // find the next occurrence offset = styleRegex.indexIn(cleanHtml, offset); } } MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus RichTextComposerNg::convertPlainText(MessageComposer::TextPart *textPart) { Q_UNUSED(textPart); return MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus::NotConverted; } #define USE_TEXTHTML_BUILDER 1 void RichTextComposerNg::fillComposerTextPart(MessageComposer::TextPart *textPart) { bool wasConverted = convertPlainText(textPart) == MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus::Converted; if (composerControler()->isFormattingUsed()) { if (!wasConverted) { if (MessageComposer::MessageComposerSettings::self()->improvePlainTextOfHtmlMessage()) { KPIMTextEdit::PlainTextMarkupBuilder *pb = new KPIMTextEdit::PlainTextMarkupBuilder(); KPIMTextEdit::MarkupDirector *pmd = new KPIMTextEdit::MarkupDirector(pb); pmd->processDocument(document()); const QString plainText = pb->getResult(); textPart->setCleanPlainText(composerControler()->toCleanPlainText(plainText)); QTextDocument *doc = new QTextDocument(plainText); doc->adjustSize(); textPart->setWrappedPlainText(composerControler()->toWrappedPlainText(doc)); delete doc; delete pmd; delete pb; } else { textPart->setCleanPlainText(composerControler()->toCleanPlainText()); textPart->setWrappedPlainText(composerControler()->toWrappedPlainText()); } } } else { if (!wasConverted) { textPart->setCleanPlainText(composerControler()->toCleanPlainText()); textPart->setWrappedPlainText(composerControler()->toWrappedPlainText()); } } textPart->setWordWrappingEnabled(lineWrapMode() == QTextEdit::FixedColumnWidth); if (composerControler()->isFormattingUsed() && !wasConverted) { #ifdef USE_TEXTHTML_BUILDER KPIMTextEdit::TextHTMLBuilder *pb = new KPIMTextEdit::TextHTMLBuilder(); KPIMTextEdit::MarkupDirector *pmd = new KPIMTextEdit::MarkupDirector(pb); pmd->processDocument(document()); QString cleanHtml = QStringLiteral("\n\n\n\n%1\n").arg(pb->getResult()); delete pmd; delete pb; d->fixHtmlFontSize(cleanHtml); textPart->setCleanHtml(cleanHtml); //qDebug() << " cleanHtml grantlee builder" << cleanHtml; #else QString cleanHtml = d->toCleanHtml(); d->fixHtmlFontSize(cleanHtml); textPart->setCleanHtml(cleanHtml); qDebug() << "cleanHtml " << cleanHtml; #endif textPart->setEmbeddedImages(composerControler()->composerImages()->embeddedImages()); } } QString RichTextComposerNgPrivate::toCleanHtml() const { QString result = richtextComposer->toHtml(); static const QString EMPTYLINEHTML = QStringLiteral( "

 

"); // Qt inserts various style properties based on the current mode of the editor (underline, // bold, etc), but only empty paragraphs *also* have qt-paragraph-type set to 'empty'. static const QString EMPTYLINEREGEX = QStringLiteral( "

"); static const QString OLLISTPATTERNQT = QStringLiteral( "

    elements with

     

    . QRegExp emptyLineFinder(EMPTYLINEREGEX); emptyLineFinder.setMinimal(true); // find the first occurrence int offset = emptyLineFinder.indexIn(result, 0); while (offset != -1) { // replace all the matching text with the new line text result.replace(offset, emptyLineFinder.matchedLength(), EMPTYLINEHTML); // advance the search offset to just beyond the last replace offset += EMPTYLINEHTML.length(); // find the next occurrence offset = emptyLineFinder.indexIn(result, offset); } // fix 2a - ordered lists - MS Outlook treats margin-left:0px; as // a non-existing number; e.g: "1. First item" turns into "First Item" result.replace(OLLISTPATTERNQT, ORDEREDLISTHTML); // fix 2b - unordered lists - MS Outlook treats margin-left:0px; as // a non-existing bullet; e.g: "* First bullet" turns into "First Bullet" result.replace(ULLISTPATTERNQT, UNORDEREDLISTHTML); return result; } static bool isCursorAtEndOfLine(const QTextCursor &cursor) { QTextCursor testCursor = cursor; testCursor.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor); return !testCursor.hasSelection(); } static void insertSignatureHelper(const QString &signature, RichTextComposerNg *textEdit, KIdentityManagement::Signature::Placement placement, bool isHtml, bool addNewlines) { if (!signature.isEmpty()) { // Save the modified state of the document, as inserting a signature // shouldn't change this. Restore it at the end of this function. bool isModified = textEdit->document()->isModified(); // Move to the desired position, where the signature should be inserted QTextCursor cursor = textEdit->textCursor(); QTextCursor oldCursor = cursor; cursor.beginEditBlock(); if (placement == KIdentityManagement::Signature::End) { cursor.movePosition(QTextCursor::End); } else if (placement == KIdentityManagement::Signature::Start) { cursor.movePosition(QTextCursor::Start); } else if (placement == KIdentityManagement::Signature::AtCursor) { cursor.movePosition(QTextCursor::StartOfLine); } textEdit->setTextCursor(cursor); QString lineSep; if (addNewlines) { if (isHtml) { lineSep = QStringLiteral("
    "); } else { lineSep = QLatin1Char('\n'); } } // Insert the signature and newlines depending on where it was inserted. int newCursorPos = -1; QString headSep; QString tailSep; if (placement == KIdentityManagement::Signature::End) { // There is one special case when re-setting the old cursor: The cursor // was at the end. In this case, QTextEdit has no way to know // if the signature was added before or after the cursor, and just // decides that it was added before (and the cursor moves to the end, // but it should not when appending a signature). See bug 167961 if (oldCursor.position() == textEdit->toPlainText().length()) { newCursorPos = oldCursor.position(); } headSep = lineSep; } else if (placement == KIdentityManagement::Signature::Start) { // When prepending signatures, add a couple of new lines before // the signature, and move the cursor to the beginning of the QTextEdit. // People tends to insert new text there. newCursorPos = 0; headSep = lineSep + lineSep; if (!isCursorAtEndOfLine(cursor)) { tailSep = lineSep; } } else if (placement == KIdentityManagement::Signature::AtCursor) { if (!isCursorAtEndOfLine(cursor)) { tailSep = lineSep; } } const QString full_signature = headSep + signature + tailSep; if (isHtml) { textEdit->insertHtml(full_signature); } else { textEdit->insertPlainText(full_signature); } cursor.endEditBlock(); if (newCursorPos != -1) { oldCursor.setPosition(newCursorPos); } textEdit->setTextCursor(oldCursor); textEdit->ensureCursorVisible(); textEdit->document()->setModified(isModified); if (isHtml) { textEdit->activateRichText(); } } } void RichTextComposerNg::insertSignature(const KIdentityManagement::Signature &signature, KIdentityManagement::Signature::Placement placement, KIdentityManagement::Signature::AddedText addedText) { if (signature.isEnabledSignature()) { QString signatureStr; if (addedText & KIdentityManagement::Signature::AddSeparator) { signatureStr = signature.withSeparator(); } else { signatureStr = signature.rawText(); } insertSignatureHelper(signatureStr, this, placement, (signature.isInlinedHtml() && signature.type() == KIdentityManagement::Signature::Inlined), (addedText & KIdentityManagement::Signature::AddNewLines)); // We added the text of the signature above, now it is time to add the images as well. if (signature.isInlinedHtml()) { - const QList embeddedImages = signature.embeddedImages(); + const QVector embeddedImages = signature.embeddedImages(); for (const KIdentityManagement::Signature::EmbeddedImagePtr &image : embeddedImages) { composerControler()->composerImages()->loadImage(image->image, image->name, image->name); } } } } QString RichTextComposerNg::toCleanHtml() const { return d->toCleanHtml(); } void RichTextComposerNg::forceAutoCorrection(bool selectedText) { if (document()->isEmpty()) { return; } if (d->autoCorrection && d->autoCorrection->isEnabledAutoCorrection()) { const bool richText = (textMode() == RichTextComposer::Rich); const int initialPosition = textCursor().position(); QTextCursor cur = textCursor(); cur.beginEditBlock(); if (selectedText && cur.hasSelection()) { const int positionStart = qMin(cur.selectionEnd(), cur.selectionStart()); const int positionEnd = qMax(cur.selectionEnd(), cur.selectionStart()); cur.setPosition(positionStart); int cursorPosition = positionStart; while (cursorPosition < positionEnd) { if (isLineQuoted(cur.block().text())) { cur.movePosition(QTextCursor::NextBlock); } else { cur.movePosition(QTextCursor::NextWord); } cursorPosition = cur.position(); (void)d->autoCorrection->autocorrect(richText, *document(), cursorPosition); } } else { cur.movePosition(QTextCursor::Start); while (!cur.atEnd()) { if (isLineQuoted(cur.block().text())) { cur.movePosition(QTextCursor::NextBlock); } else { cur.movePosition(QTextCursor::NextWord); } int cursorPosition = cur.position(); (void)d->autoCorrection->autocorrect(richText, *document(), cursorPosition); } } cur.endEditBlock(); if (cur.position() != initialPosition) { cur.setPosition(initialPosition); setTextCursor(cur); } } } diff --git a/messagecomposer/src/composer/composerviewinterface.cpp b/messagecomposer/src/composer/composerviewinterface.cpp index e3d5dd0c..52ac6762 100644 --- a/messagecomposer/src/composer/composerviewinterface.cpp +++ b/messagecomposer/src/composer/composerviewinterface.cpp @@ -1,120 +1,120 @@ /* Copyright (C) 2019-2020 Laurent Montel 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 "composerviewinterface.h" #include "composerviewbase.h" #include #include using namespace MessageComposer; ComposerViewInterface::ComposerViewInterface(ComposerViewBase *composerView) : mComposerView(composerView) { } ComposerViewInterface::~ComposerViewInterface() { } QString ComposerViewInterface::subject() const { if (mComposerView) { return mComposerView->subject(); } return {}; } QString ComposerViewInterface::to() const { if (mComposerView) { return mComposerView->to(); } return {}; } QString ComposerViewInterface::cc() const { if (mComposerView) { return mComposerView->cc(); } return {}; } QString ComposerViewInterface::from() const { if (mComposerView) { return mComposerView->from(); } return {}; } MessageComposer::ComposerAttachmentInterface ComposerViewInterface::attachments() { MessageComposer::ComposerAttachmentInterface attachmentInterface; if (mComposerView) { const int countElement = mComposerView->attachmentModel()->attachments().count(); attachmentInterface.setCount(countElement); QStringList fileNames; QStringList nameAndSize; QStringList names; fileNames.reserve(countElement); nameAndSize.reserve(countElement); names.reserve(countElement); for (const MessageCore::AttachmentPart::Ptr &attachment : mComposerView->attachmentModel()->attachments()) { fileNames.append(attachment->fileName()); names.append(attachment->name()); - nameAndSize.append(QStringLiteral("%1 (%2)").arg(attachment->name()).arg(KFormat().formatByteSize(attachment->size()))); + nameAndSize.append(QStringLiteral("%1 (%2)").arg(attachment->name(), KFormat().formatByteSize(attachment->size()))); } attachmentInterface.setNames(names); attachmentInterface.setNamesAndSize(nameAndSize); attachmentInterface.setFileNames(fileNames); } return attachmentInterface; } QString ComposerViewInterface::shortDate() const { QLocale locale; return locale.toString(QDate::currentDate(), QLocale::ShortFormat); } QString ComposerViewInterface::longDate() const { QLocale locale; return locale.toString(QDate::currentDate(), QLocale::LongFormat); } QString ComposerViewInterface::shortTime() const { QLocale locale; return locale.toString(QTime::currentTime(), QLocale::ShortFormat); } QString ComposerViewInterface::longTime() const { QLocale locale; return locale.toString(QTime::currentTime(), QLocale::LongFormat); } QString ComposerViewInterface::insertDayOfWeek() const { const QDateTime date = QDateTime::currentDateTime().toLocalTime(); const QString str = QLocale().dayName(date.date().dayOfWeek(), QLocale::LongFormat); return str; } diff --git a/messagecomposer/src/job/contentjobbase.cpp b/messagecomposer/src/job/contentjobbase.cpp index 3a8d748e..4ec747e8 100644 --- a/messagecomposer/src/job/contentjobbase.cpp +++ b/messagecomposer/src/job/contentjobbase.cpp @@ -1,131 +1,131 @@ /* Copyright (c) 2009 Constantin Berzan 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 "contentjobbase.h" #include "contentjobbase_p.h" #include "messagecomposer_debug.h" #include using namespace MessageComposer; void ContentJobBasePrivate::init(QObject *parent) { Q_Q(ContentJobBase); - ContentJobBase *parentJob = dynamic_cast(parent); + ContentJobBase *parentJob = qobject_cast(parent); if (parentJob) { if (!parentJob->appendSubjob(q)) { qCWarning(MESSAGECOMPOSER_LOG) << "Impossible to add subjob."; } } } void ContentJobBasePrivate::doNextSubjob() { Q_Q(ContentJobBase); if (q->hasSubjobs()) { q->subjobs().first()->start(); } else { qCDebug(MESSAGECOMPOSER_LOG) << "Calling process."; q->process(); } } ContentJobBase::ContentJobBase(QObject *parent) : JobBase(*new ContentJobBasePrivate(this), parent) { Q_D(ContentJobBase); d->init(parent); } ContentJobBase::ContentJobBase(ContentJobBasePrivate &dd, QObject *parent) : JobBase(dd, parent) { Q_D(ContentJobBase); d->init(parent); } ContentJobBase::~ContentJobBase() { } void ContentJobBase::start() { doStart(); } KMime::Content *ContentJobBase::content() const { Q_D(const ContentJobBase); //Q_ASSERT( !hasSubjobs() ); // Finished. // JobBase::hasSubjobs is not const :-/ TODO const_cast?? Q_ASSERT(d->resultContent); // process() should do something. return d->resultContent; } bool ContentJobBase::appendSubjob(ContentJobBase *job) { job->setParent(this); return KCompositeJob::addSubjob(job); } void ContentJobBase::setExtraContent(KMime::Content *extra) { Q_D(ContentJobBase); d->extraContent = extra; } KMime::Content *ContentJobBase::extraContent() const { Q_D(const ContentJobBase); return d->extraContent; } bool ContentJobBase::addSubjob(KJob *job) { Q_UNUSED(job); qCCritical(MESSAGECOMPOSER_LOG) << "Use appendJob() instead."; Q_ASSERT(false); return false; } void ContentJobBase::doStart() { Q_D(ContentJobBase); Q_ASSERT(d->resultContent == nullptr && d->subjobContents.isEmpty()); // Not started. Q_ASSERT(!error()); // Jobs emitting an error in doStart should not call ContentJobBase::doStart(). d->doNextSubjob(); } void ContentJobBase::slotResult(KJob *job) { Q_D(ContentJobBase); KCompositeJob::slotResult(job); // Handles errors and removes subjob. qCDebug(MESSAGECOMPOSER_LOG) << "A subjob finished." << subjobs().count() << "more to go."; if (error()) { return; } Q_ASSERT(dynamic_cast(job)); ContentJobBase *cjob = static_cast(job); d->subjobContents.append(cjob->content()); d->doNextSubjob(); } diff --git a/messagecomposer/src/job/encryptjob.cpp b/messagecomposer/src/job/encryptjob.cpp index d0f7f3ab..d3acd963 100644 --- a/messagecomposer/src/job/encryptjob.cpp +++ b/messagecomposer/src/job/encryptjob.cpp @@ -1,264 +1,264 @@ /* Copyright (C) 2009 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.net Copyright (c) 2009 Leo Franchi 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 "job/encryptjob.h" #include "contentjobbase_p.h" #include "job/protectedheaders.h" #include "utils/util_p.h" #include #include #include #include "messagecomposer_debug.h" #include #include #include #include using namespace MessageComposer; class MessageComposer::EncryptJobPrivate : public ContentJobBasePrivate { public: EncryptJobPrivate(EncryptJob *qq) : ContentJobBasePrivate(qq) { } QStringList recipients; std::vector keys; Kleo::CryptoMessageFormat format; KMime::Content *content = nullptr; KMime::Message *skeletonMessage = nullptr; bool protectedHeaders = true; bool protectedHeadersObvoscate = false; // copied from messagecomposer.cpp bool binaryHint(Kleo::CryptoMessageFormat f) { switch (f) { case Kleo::SMIMEFormat: case Kleo::SMIMEOpaqueFormat: return true; default: case Kleo::OpenPGPMIMEFormat: case Kleo::InlineOpenPGPFormat: return false; } } GpgME::SignatureMode signingMode(Kleo::CryptoMessageFormat f) { switch (f) { case Kleo::SMIMEOpaqueFormat: return GpgME::NormalSignatureMode; case Kleo::InlineOpenPGPFormat: return GpgME::Clearsigned; default: case Kleo::SMIMEFormat: case Kleo::OpenPGPMIMEFormat: return GpgME::Detached; } } Q_DECLARE_PUBLIC(EncryptJob) }; EncryptJob::EncryptJob(QObject *parent) : ContentJobBase(*new EncryptJobPrivate(this), parent) { } EncryptJob::~EncryptJob() { } void EncryptJob::setContent(KMime::Content *content) { Q_D(EncryptJob); d->content = content; d->content->assemble(); } void EncryptJob::setCryptoMessageFormat(Kleo::CryptoMessageFormat format) { Q_D(EncryptJob); d->format = format; } void EncryptJob::setEncryptionKeys(const std::vector &keys) { Q_D(EncryptJob); d->keys = keys; } void EncryptJob::setRecipients(const QStringList &recipients) { Q_D(EncryptJob); d->recipients = recipients; } void EncryptJob::setSkeletonMessage(KMime::Message* skeletonMessage) { Q_D(EncryptJob); d->skeletonMessage = skeletonMessage; } void EncryptJob::setProtectedHeaders(bool protectedHeaders) { Q_D(EncryptJob); d->protectedHeaders = protectedHeaders; } void EncryptJob::setProtectedHeadersObvoscate(bool protectedHeadersObvoscate) { Q_D(EncryptJob); d->protectedHeadersObvoscate = protectedHeadersObvoscate; } QStringList EncryptJob::recipients() const { Q_D(const EncryptJob); return d->recipients; } std::vector EncryptJob::encryptionKeys() const { Q_D(const EncryptJob); return d->keys; } void EncryptJob::doStart() { Q_D(EncryptJob); Q_ASSERT(d->resultContent == nullptr); // Not processed before. if (d->keys.size() == 0) { // should not happen---resolver should have dealt with it earlier qCDebug(MESSAGECOMPOSER_LOG) << "HELP! Encrypt job but have no keys to encrypt with."; return; } // if setContent hasn't been called, we assume that a subjob was added // and we want to use that if (!d->content || !d->content->hasContent()) { if (d->subjobContents.size() == 1) { d->content = d->subjobContents.constFirst(); } } if (d->protectedHeaders && d->skeletonMessage && d->format & Kleo::OpenPGPMIMEFormat) { ProtectedHeadersJob *pJob = new ProtectedHeadersJob; pJob->setContent(d->content); pJob->setSkeletonMessage(d->skeletonMessage); pJob->setObvoscate(d->protectedHeadersObvoscate); QObject::connect(pJob, &ProtectedHeadersJob::finished, this, [d, pJob](KJob *job) { if (job->error()) { return; } d->content = pJob->content(); }); appendSubjob(pJob); } ContentJobBase::doStart(); } void EncryptJob::slotResult(KJob *job) { - Q_D(EncryptJob); + //Q_D(EncryptJob); if (error()) { ContentJobBase::slotResult(job); return; } if (subjobs().size() == 2) { auto pjob = static_cast(subjobs().last()); if (pjob) { auto cjob = qobject_cast(job); Q_ASSERT(cjob); pjob->setContent(cjob->content()); } } ContentJobBase::slotResult(job); } void EncryptJob::process() { Q_D(EncryptJob); // if setContent hasn't been called, we assume that a subjob was added // and we want to use that if (!d->content || !d->content->hasContent()) { Q_ASSERT(d->subjobContents.size() == 1); d->content = d->subjobContents.constFirst(); } const QGpgME::Protocol *proto = nullptr; if (d->format & Kleo::AnyOpenPGP) { proto = QGpgME::openpgp(); } else if (d->format & Kleo::AnySMIME) { proto = QGpgME::smime(); } else { qCDebug(MESSAGECOMPOSER_LOG) << "HELP! Encrypt job but have protocol to encrypt with."; return; } Q_ASSERT(proto); // for now just do the main recipients QByteArray encryptedBody; QByteArray content; d->content->assemble(); if (d->format & Kleo::InlineOpenPGPFormat) { content = d->content->body(); } else { content = d->content->encodedContent(); } qCDebug(MESSAGECOMPOSER_LOG) << "got backend, starting job"; QGpgME::EncryptJob *eJob = proto->encryptJob(!d->binaryHint(d->format), d->format == Kleo::InlineOpenPGPFormat); QObject::connect(eJob, &QGpgME::EncryptJob::result, this, [this, d](const GpgME::EncryptionResult &result, const QByteArray &cipherText, const QString &auditLogAsHtml, const GpgME::Error &auditLogError) { if (result.error()) { setError(result.error().code()); setErrorText(QString::fromLocal8Bit(result.error().asString())); emitResult(); return; } d->resultContent = MessageComposer::Util::composeHeadersAndBody(d->content, cipherText, d->format, false); emitResult(); }); eJob->start(d->keys, content, true); } diff --git a/messagecomposer/src/job/signencryptjob.cpp b/messagecomposer/src/job/signencryptjob.cpp index 4a3c9a41..063b2568 100644 --- a/messagecomposer/src/job/signencryptjob.cpp +++ b/messagecomposer/src/job/signencryptjob.cpp @@ -1,282 +1,282 @@ /* Copyright (C) 2009 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.net Copyright (c) 2009 Leo Franchi 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 "job/signencryptjob.h" #include "contentjobbase_p.h" #include "job/protectedheaders.h" #include "utils/util_p.h" #include #include #include #include "messagecomposer_debug.h" #include #include #include #include #include #include #include using namespace MessageComposer; class MessageComposer::SignEncryptJobPrivate : public ContentJobBasePrivate { public: SignEncryptJobPrivate(SignEncryptJob *qq) : ContentJobBasePrivate(qq) { } std::vector signers; std::vector encKeys; QStringList recipients; Kleo::CryptoMessageFormat format; KMime::Content *content = nullptr; KMime::Message *skeletonMessage = nullptr; bool protectedHeaders = true; bool protectedHeadersObvoscate = false; // copied from messagecomposer.cpp bool binaryHint(Kleo::CryptoMessageFormat f) { switch (f) { case Kleo::SMIMEFormat: case Kleo::SMIMEOpaqueFormat: return true; default: case Kleo::OpenPGPMIMEFormat: case Kleo::InlineOpenPGPFormat: return false; } } Q_DECLARE_PUBLIC(SignEncryptJob) }; SignEncryptJob::SignEncryptJob(QObject *parent) : ContentJobBase(*new SignEncryptJobPrivate(this), parent) { } SignEncryptJob::~SignEncryptJob() { } void SignEncryptJob::setContent(KMime::Content *content) { Q_D(SignEncryptJob); Q_ASSERT(content); d->content = content; } void SignEncryptJob::setCryptoMessageFormat(Kleo::CryptoMessageFormat format) { Q_D(SignEncryptJob); // There *must* be a concrete format set at this point. Q_ASSERT(format == Kleo::OpenPGPMIMEFormat || format == Kleo::InlineOpenPGPFormat || format == Kleo::SMIMEFormat || format == Kleo::SMIMEOpaqueFormat); d->format = format; } void SignEncryptJob::setSigningKeys(const std::vector &signers) { Q_D(SignEncryptJob); d->signers = signers; } KMime::Content *SignEncryptJob::origContent() { Q_D(SignEncryptJob); return d->content; } void SignEncryptJob::setEncryptionKeys(const std::vector &keys) { Q_D(SignEncryptJob); d->encKeys = keys; } void SignEncryptJob::setRecipients(const QStringList &recipients) { Q_D(SignEncryptJob); d->recipients = recipients; } void SignEncryptJob::setSkeletonMessage(KMime::Message *skeletonMessage) { Q_D(SignEncryptJob); d->skeletonMessage = skeletonMessage; } void SignEncryptJob::setProtectedHeaders(bool protectedHeaders) { Q_D(SignEncryptJob); d->protectedHeaders = protectedHeaders; } void SignEncryptJob::setProtectedHeadersObvoscate(bool protectedHeadersObvoscate) { Q_D(SignEncryptJob); d->protectedHeadersObvoscate = protectedHeadersObvoscate; } QStringList SignEncryptJob::recipients() const { Q_D(const SignEncryptJob); return d->recipients; } std::vector SignEncryptJob::encryptionKeys() const { Q_D(const SignEncryptJob); return d->encKeys; } void SignEncryptJob::doStart() { Q_D(SignEncryptJob); Q_ASSERT(d->resultContent == nullptr); // Not processed before. if (d->protectedHeaders && d->skeletonMessage && d->format & Kleo::OpenPGPMIMEFormat) { ProtectedHeadersJob *pJob = new ProtectedHeadersJob; pJob->setContent(d->content); pJob->setSkeletonMessage(d->skeletonMessage); pJob->setObvoscate(d->protectedHeadersObvoscate); QObject::connect(pJob, &ProtectedHeadersJob::finished, this, [d, pJob](KJob *job) { if (job->error()) { return; } d->content = pJob->content(); }); appendSubjob(pJob); } ContentJobBase::doStart(); } void SignEncryptJob::slotResult(KJob *job) { - Q_D(SignEncryptJob); + //Q_D(SignEncryptJob); if (error()) { ContentJobBase::slotResult(job); return; } if (subjobs().size() == 2) { auto pjob = static_cast(subjobs().last()); if (pjob) { auto cjob = dynamic_cast(job); Q_ASSERT(cjob); pjob->setContent(cjob->content()); } } ContentJobBase::slotResult(job); } void SignEncryptJob::process() { Q_D(SignEncryptJob); Q_ASSERT(d->resultContent == nullptr); // Not processed before. // if setContent hasn't been called, we assume that a subjob was added // and we want to use that if (!d->content || !d->content->hasContent()) { Q_ASSERT(d->subjobContents.size() == 1); - d->content = d->subjobContents.first(); + d->content = d->subjobContents.constFirst(); } const QGpgME::Protocol *proto = nullptr; if (d->format & Kleo::AnyOpenPGP) { proto = QGpgME::openpgp(); } else if (d->format & Kleo::AnySMIME) { proto = QGpgME::smime(); } else { return; } Q_ASSERT(proto); //d->resultContent = new KMime::Content; qCDebug(MESSAGECOMPOSER_LOG) << "creating signencrypt from:" << proto->name() << proto->displayName(); std::unique_ptr job(proto->signEncryptJob(!d->binaryHint(d->format), d->format == Kleo::InlineOpenPGPFormat)); QByteArray encBody; d->content->assemble(); // replace simple LFs by CRLFs for all MIME supporting CryptPlugs // according to RfC 2633, 3.1.1 Canonicalization QByteArray content; if (d->format & Kleo::InlineOpenPGPFormat) { content = d->content->body(); } else if (!(d->format & Kleo::SMIMEOpaqueFormat)) { content = KMime::LFtoCRLF(d->content->encodedContent()); } else { // SMimeOpaque doesn't need LFtoCRLF, else it gets munged content = d->content->encodedContent(); } // FIXME: Make this async const std::pair res = job->exec(d->signers, d->encKeys, content, false, encBody); // exec'ed jobs don't delete themselves job->deleteLater(); if (res.first.error()) { qCDebug(MESSAGECOMPOSER_LOG) << "signing failed:" << res.first.error().asString(); setError(res.first.error().code()); setErrorText(QString::fromLocal8Bit(res.first.error().asString())); emitResult(); return; } if (res.second.error()) { qCDebug(MESSAGECOMPOSER_LOG) << "encrypting failed:" << res.second.error().asString(); setError(res.second.error().code()); setErrorText(QString::fromLocal8Bit(res.second.error().asString())); emitResult(); return; } QByteArray signatureHashAlgo = res.first.createdSignature(0).hashAlgorithmAsString(); d->resultContent = MessageComposer::Util::composeHeadersAndBody(d->content, encBody, d->format, false, signatureHashAlgo); emitResult(); } diff --git a/messagecomposer/src/recipient/recipient.h b/messagecomposer/src/recipient/recipient.h index 1912fac0..484de856 100644 --- a/messagecomposer/src/recipient/recipient.h +++ b/messagecomposer/src/recipient/recipient.h @@ -1,85 +1,85 @@ /* Copyright (c) 2010 Volker Krause Based in kmail/recipientseditor.h/cpp Copyright (c) 2004 Cornelius Schumacher 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. As a special exception, permission is given to link this program with any edition of Qt, and distribute the resulting executable, without including the source code for Qt in the source distribution. */ #ifndef MESSAGECOMPOSER_RECIPIENT_H #define MESSAGECOMPOSER_RECIPIENT_H #include "messagecomposer_export.h" #include #include #include #include #include namespace MessageComposer { /** Represents a mail recipient. */ class RecipientPrivate; /** * @brief The Recipient class */ class MESSAGECOMPOSER_EXPORT Recipient : public KPIM::MultiplyingLineData { public: typedef QSharedPointer Ptr; - typedef QList List; + typedef QVector List; enum Type { To, Cc, Bcc, ReplyTo, Undefined }; Recipient(const QString &email = QString(), Type type = To); //krazy:exclude=explicit ~Recipient() override; void setType(Type type); Q_REQUIRED_RESULT Type type() const; void setEmail(const QString &email); Q_REQUIRED_RESULT QString email() const; void setName(const QString &name); Q_REQUIRED_RESULT QString name() const; Q_REQUIRED_RESULT bool isEmpty() const override; void clear() override; Q_REQUIRED_RESULT static int typeToId(Type type); Q_REQUIRED_RESULT static Type idToType(int id); Q_REQUIRED_RESULT QString typeLabel() const; Q_REQUIRED_RESULT static QString typeLabel(Type type); Q_REQUIRED_RESULT static QStringList allTypeLabels(); void setEncryptionAction(const Kleo::Action action); Q_REQUIRED_RESULT Kleo::Action encryptionAction() const; void setKey(const GpgME::Key &key); Q_REQUIRED_RESULT GpgME::Key key() const; private: RecipientPrivate *const d; }; } #endif diff --git a/messagecomposer/src/recipient/recipientseditor.cpp b/messagecomposer/src/recipient/recipientseditor.cpp index a95911e9..068e6b65 100644 --- a/messagecomposer/src/recipient/recipientseditor.cpp +++ b/messagecomposer/src/recipient/recipientseditor.cpp @@ -1,379 +1,379 @@ /* Copyright (C) 2010 Casey Link Copyright (C) 2009-2010 Klaralvdalens Datakonsult AB, a KDAB Group company Refactored from earlier code by: Copyright (c) 2010 Volker Krause Copyright (c) 2004 Cornelius Schumacher 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 "recipientseditor.h" #include "recipient.h" #include "recipientline.h" #include "recipientseditorsidewidget.h" #include "settings/messagecomposersettings.h" #include "distributionlistdialog.h" #include "messagecomposer_debug.h" #include #include #include #include #include #include using namespace MessageComposer; using namespace KPIM; RecipientLineFactory::RecipientLineFactory(QObject *parent) : KPIM::MultiplyingLineFactory(parent) { } KPIM::MultiplyingLine *RecipientLineFactory::newLine(QWidget *p) { RecipientLineNG *line = new RecipientLineNG(p); if (qobject_cast(parent())) { connect(line, SIGNAL(addRecipient(RecipientLineNG*,QString)), qobject_cast(parent()), SLOT(addRecipient(RecipientLineNG*,QString))); } else { qCWarning(MESSAGECOMPOSER_LOG) << "RecipientLineFactory::newLine: We can't connect to new line" << parent(); } return line; } int RecipientLineFactory::maximumRecipients() { return MessageComposer::MessageComposerSettings::self()->maximumRecipients(); } class MessageComposer::RecipientsEditorPrivate { public: RecipientsEditorPrivate() { } KConfig *mRecentAddressConfig = nullptr; RecipientsEditorSideWidget *mSideWidget = nullptr; bool mSkipTotal = false; }; RecipientsEditor::RecipientsEditor(QWidget *parent) : RecipientsEditor(new RecipientLineFactory(nullptr), parent) { } RecipientsEditor::RecipientsEditor(RecipientLineFactory *lineFactory, QWidget *parent) : MultiplyingLineEditor(lineFactory, parent) , d(new MessageComposer::RecipientsEditorPrivate) { factory()->setParent(this); // HACK: can't use 'this' above since it's not yet constructed at that point d->mSideWidget = new RecipientsEditorSideWidget(this, this); layout()->addWidget(d->mSideWidget); // Install global event filter and listen for keypress events for RecipientLineEdits. // Unfortunately we can't install ourselves directly as event filter for the edits, // because the RecipientLineEdit has its own event filter installed into QApplication // and so it would eat the event before it would reach us. qApp->installEventFilter(this); connect(d->mSideWidget, &RecipientsEditorSideWidget::pickedRecipient, this, &RecipientsEditor::slotPickedRecipient); connect(d->mSideWidget, &RecipientsEditorSideWidget::saveDistributionList, this, &RecipientsEditor::saveDistributionList); connect(this, &RecipientsEditor::lineAdded, this, &RecipientsEditor::slotLineAdded); connect(this, &RecipientsEditor::lineDeleted, this, &RecipientsEditor::slotLineDeleted); addData(); // one default line } RecipientsEditor::~RecipientsEditor() { delete d; } bool RecipientsEditor::addRecipient(const QString &recipient, Recipient::Type type) { return addData(Recipient::Ptr(new Recipient(recipient, type))); } void RecipientsEditor::addRecipient(RecipientLineNG *line, const QString &recipient) { addRecipient(recipient, line->recipientType()); } void RecipientsEditor::setRecipientString(const QVector< KMime::Types::Mailbox > &mailboxes, Recipient::Type type) { int count = 1; for (const KMime::Types::Mailbox &mailbox : mailboxes) { if (count++ > MessageComposer::MessageComposerSettings::self()->maximumRecipients()) { KMessageBox::sorry(this, i18ncp("@info:status", "Truncating recipients list to %2 of %1 entry.", "Truncating recipients list to %2 of %1 entries.", mailboxes.count(), MessageComposer::MessageComposerSettings::self()->maximumRecipients())); break; } addRecipient(mailbox.prettyAddress(KMime::Types::Mailbox::QuoteWhenNecessary), type); } } Recipient::List RecipientsEditor::recipients() const { const QList dataList = allData(); Recipient::List recList; for (const MultiplyingLineData::Ptr &datum : dataList) { Recipient::Ptr rec = qSharedPointerDynamicCast(datum); if (!rec) { continue; } recList << rec; } return recList; } Recipient::Ptr RecipientsEditor::activeRecipient() const { return qSharedPointerDynamicCast(activeData()); } QString RecipientsEditor::recipientString(Recipient::Type type) const { return recipientStringList(type).join(QLatin1String(", ")); } QStringList RecipientsEditor::recipientStringList(Recipient::Type type) const { QStringList selectedRecipients; for (const Recipient::Ptr &r : recipients()) { if (r->type() == type) { selectedRecipients << r->email(); } } return selectedRecipients; } void RecipientsEditor::removeRecipient(const QString &recipient, Recipient::Type type) { // search a line which matches recipient and type QListIterator it(lines()); MultiplyingLine *line = nullptr; while (it.hasNext()) { line = it.next(); RecipientLineNG *rec = qobject_cast< RecipientLineNG * >(line); if (rec) { if ((rec->recipient()->email() == recipient) && (rec->recipientType() == type)) { break; } } } if (line) { line->slotPropagateDeletion(); } } void RecipientsEditor::saveDistributionList() { std::unique_ptr dlg(new MessageComposer::DistributionListDialog(this)); dlg->setRecipients(recipients()); dlg->exec(); } void RecipientsEditor::selectRecipients() { d->mSideWidget->pickRecipient(); } void MessageComposer::RecipientsEditor::setRecentAddressConfig(KConfig *config) { d->mRecentAddressConfig = config; if (config) { MultiplyingLine *line; foreach (line, lines()) { RecipientLineNG *rec = qobject_cast< RecipientLineNG * >(line); if (rec) { rec->setRecentAddressConfig(config); } } } } void MessageComposer::RecipientsEditor::slotPickedRecipient(const Recipient &rec, bool &tooManyAddress) { Recipient::Type t = rec.type(); tooManyAddress = addRecipient(rec.email(), t == Recipient::Undefined ? Recipient::To : t); mModified = true; } RecipientsPicker *RecipientsEditor::picker() const { return d->mSideWidget->picker(); } void RecipientsEditor::slotLineAdded(MultiplyingLine *line) { // subtract 1 here, because we want the number of lines // before this line was added. int count = lines().size() - 1; RecipientLineNG *rec = qobject_cast(line); if (!rec) { return; } if (d->mRecentAddressConfig) { rec->setRecentAddressConfig(d->mRecentAddressConfig); } if (count > 0) { if (count == 1) { - RecipientLineNG *last_rec = qobject_cast< RecipientLineNG * >(lines().first()); + RecipientLineNG *last_rec = qobject_cast< RecipientLineNG * >(lines().constFirst()); if (last_rec && (last_rec->recipientType() == Recipient::Bcc || last_rec->recipientType() == Recipient::ReplyTo)) { rec->setRecipientType(Recipient::To); } else { rec->setRecipientType(Recipient::Cc); } } else { RecipientLineNG *last_rec = qobject_cast< RecipientLineNG * >(lines().at(lines().count() - 2)); if (last_rec) { if (last_rec->recipientType() == Recipient::ReplyTo) { rec->setRecipientType(Recipient::To); } else { rec->setRecipientType(last_rec->recipientType()); } } } line->fixTabOrder(lines().constLast()->tabOut()); } connect(rec, &RecipientLineNG::countChanged, this, &RecipientsEditor::slotCalculateTotal); } void RecipientsEditor::slotLineDeleted(int pos) { bool atLeastOneToLine = false; int firstCC = -1; for (int i = pos, total = lines().count(); i < total; ++i) { MultiplyingLine *line = lines().at(i); RecipientLineNG *rec = qobject_cast< RecipientLineNG * >(line); if (rec) { if (rec->recipientType() == Recipient::To) { atLeastOneToLine = true; } else if ((rec->recipientType() == Recipient::Cc) && (firstCC < 0)) { firstCC = i; } } } if (!atLeastOneToLine && (firstCC >= 0)) { RecipientLineNG *firstCCLine = qobject_cast< RecipientLineNG * >(lines().at(firstCC)); if (firstCCLine) { firstCCLine->setRecipientType(Recipient::To); } } slotCalculateTotal(); } bool RecipientsEditor::eventFilter(QObject *object, QEvent *event) { if (event->type() == QEvent::KeyPress && qobject_cast(object)) { auto ke = static_cast(event); // Treats comma or semicolon as email separator, will automatically move focus // to a new line, basically preventing user from inputting more than one // email address per line, which breaks our opportunistic crypto in composer if (ke->key() == Qt::Key_Comma || ( ke->key() == Qt::Key_Semicolon && MessageComposerSettings::self()->allowSemicolonAsAddressSeparator())) { auto line = qobject_cast(object->parent()); const auto split = KEmailAddress::splitAddressList(line->rawData() + QLatin1String(", ")); if (split.size() > 1) { addRecipient(QString(), line->recipientType()); setFocusBottom(); return true; } } } return false; } void RecipientsEditor::slotCalculateTotal() { // Prevent endless recursion when splitting recipient if (d->mSkipTotal) { return; } int empty = 0; MultiplyingLine *line = nullptr; foreach (line, lines()) { RecipientLineNG *rec = qobject_cast< RecipientLineNG * >(line); if (rec) { if (rec->isEmpty()) { ++empty; } else { const int recipientsCount = rec->recipientsCount(); if (recipientsCount > 1) { // Ensure we always have only one recipient per line d->mSkipTotal = true; Recipient::Ptr recipient = rec->recipient(); const auto split = KEmailAddress::splitAddressList(recipient->email()); bool maximumElementFound = false; for (int i = 1 /* sic! */; i < split.count(); ++i) { maximumElementFound = addRecipient(split[i], rec->recipientType()); if (maximumElementFound) { break; } } recipient->setEmail(split[0]); rec->setData(recipient); setFocusBottom(); // focus next empty entry d->mSkipTotal = false; if (maximumElementFound) { return; } } } } } // We always want at least one empty line if (empty == 0) { addData(); } int count = 0; foreach (line, lines()) { RecipientLineNG *rec = qobject_cast< RecipientLineNG * >(line); if (rec) { if (!rec->isEmpty()) { count++; } } } // update the side widget d->mSideWidget->setTotal(count, lines().count()); } RecipientLineNG *RecipientsEditor::activeLine() const { MultiplyingLine *line = MultiplyingLineEditor::activeLine(); return qobject_cast< RecipientLineNG * >(line); } diff --git a/messagelist/src/core/model.cpp b/messagelist/src/core/model.cpp index 77badc3c..88a68ecb 100644 --- a/messagelist/src/core/model.cpp +++ b/messagelist/src/core/model.cpp @@ -1,4633 +1,4633 @@ /****************************************************************************** * * Copyright 2008 Szymon Tomasz Stefanek * * 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. * *******************************************************************************/ // // This class is a rather huge monster. It's something that resembles a QAbstractItemModel // (because it has to provide the interface for a QTreeView) but isn't entirely one // (for optimization reasons). It basically manages a tree of items of two types: // GroupHeaderItem and MessageItem. Be sure to read the docs for ViewItemJob. // // A huge credit here goes to Till Adam which seems to have written most // (if not all) of the original KMail threading code. The KMHeaders implementation, // the documentation and his clever ideas were my starting points and essential tools. // This is why I'm adding his copyright entry (copied from headeritem.cpp) here even if // he didn't write a byte in this file until now :) // // Szymon Tomasz Stefanek, 03 Aug 2008 04:50 (am) // // This class contains ideas from: // // kmheaders.cpp / kmheaders.h, headeritem.cpp / headeritem.h // Copyright: (c) 2004 Till Adam < adam at kde dot org > // #include #include "core/model.h" #include "core/model_p.h" #include "core/view.h" #include "core/filter.h" #include "core/groupheaderitem.h" #include "core/item_p.h" #include "core/messageitem.h" #include "core/modelinvariantrowmapper.h" #include "core/storagemodelbase.h" #include "core/theme.h" #include "core/delegate.h" #include "core/manager.h" #include "core/messageitemsetmanager.h" #include "messagelist_debug.h" #include #include #include #include "MessageCore/StringUtil" #include #include #include #include #include #include #include #include #include namespace MessageList { namespace Core { Q_GLOBAL_STATIC(QTimer, _k_heartBeatTimer) /** * A job in a "View Fill" or "View Cleanup" or "View Update" task. * * For a "View Fill" task a job is a set of messages * that are contiguous in the storage. The set is expressed as a range * of row indexes. The task "sweeps" the storage in the specified * range, creates the appropriate Item instances and places them * in the right position in the tree. * * The idea is that in a single instance and for the same StorageModel * the jobs should never "cover" the same message twice. This assertion * is enforced all around this source file. * * For a "View Cleanup" task the job is a list of ModelInvariantIndex * objects (that are in fact MessageItem objects) that need to be removed * from the view. * * For a "View Update" task the job is a list of ModelInvariantIndex * objects (that are in fact MessageItem objects) that need to be updated. * * The interesting fact is that all the tasks need * very similar operations to be performed on the message tree. * * For a "View Fill" we have 5 passes. * * Pass 1 scans the underlying storage, creates the MessageItem objects * (which are subclasses of ModelInvariantIndex) and retrieves invariant * storage indexes for them. It also builds threading caches and * attempts to do some "easy" threading. If it succeeds in threading * and some conditions apply then it also attaches the items to the view. * Any unattached message is placed in a list. * * Pass 2 scans the list of messages that haven't been attached in * the first pass and performs perfect and reference based threading. * Since grouping of messages may depend on the "shape" of the thread * then certain threads aren't attached to the view yet. * Unassigned messages get stuffed into a list waiting for Pass3 * or directly to a list waiting for Pass4 (that is, Pass3 may be skipped * if there is no hope to find an imperfect parent by subject based threading). * * Pass 3 scans the list of messages that haven't been attached in * the first and second passes and performs subject based threading. * Since grouping of messages may depend on the "shape" of the thread * then certain threads aren't attached to the view yet. * Anything unattached gets stuffed into the list waiting for Pass4. * * Pass 4 scans the unattached threads and puts them in the appropriate * groups. After this pass nothing is unattached. * * Pass 5 eventually re-sorts the groups and removes the empty ones. * * For a "View Cleanup" we still have 5 passes. * * Pass 1 scans the list of invalidated ModelInvariantIndex-es, casts * them to MessageItem objects and detaches them from the view. * The orphan children of the destroyed items get stuffed in the list * of unassigned messages that has been used also in the "View Fill" task above. * * Pass 2, 3, 4 and 5: same as "View Fill", just operating on the "orphaned" * messages that need to be reattached to the view. * * For a "View Update" we still have 5 passes. * * Pass 1 scans the list of ModelInvariantIndex-es that need an update, casts * them to MessageItem objects and handles the updates from storage. * The updates may cause a regrouping so items might be stuffed in one * of the lists for pass 4 or 5. * * Pass 2, 3 and 4 are simply empty. * * Pass 5: same as "View Fill", just operating on groups that require updates * after the messages have been moved in pass 1. * * That's why we in fact have Pass1Fill, Pass1Cleanup, Pass1Update, Pass2, Pass3, Pass4 and Pass5 below. * Pass1Fill, Pass1Cleanup and Pass1Update are exclusive and all of them proceed with Pass2 when finished. */ class ViewItemJob { public: enum Pass { Pass1Fill = 0, ///< Build threading caches, *TRY* to do some threading, try to attach something to the view Pass1Cleanup = 1, ///< Kill messages, build list of orphans Pass1Update = 2, ///< Update messages Pass2 = 3, ///< Thread everything by using caches, try to attach more to the view Pass3 = 4, ///< Do more threading (this time try to guess), try to attach more to the view Pass4 = 5, ///< Attach anything is still unattached Pass5 = 6, ///< Eventually Re-sort group headers and remove the empty ones LastIndex = 7 ///< Keep this at the end, needed to get the size of the enum }; private: // Data for "View Fill" jobs int mStartIndex; ///< The first index (in the underlying storage) of this job int mCurrentIndex; ///< The current index (in the underlying storage) of this job int mEndIndex; ///< The last index (in the underlying storage) of this job // Data for "View Cleanup" jobs QList< ModelInvariantIndex * > *mInvariantIndexList; ///< Owned list of shallow pointers // Common data // The maximum time that we can spend "at once" inside viewItemJobStep() (milliseconds) // The bigger this value, the larger chunks of work we do at once and less the time // we loose in "breaking and resuming" the job. On the other side large values tend // to make the view less responsive up to a "freeze" perception if this value is larger // than 2000. int mChunkTimeout; // The interval between two fillView steps. The larger the interval, the more interactivity // we have. The shorter the interval the more work we get done per second. int mIdleInterval; // The minimum number of messages we process in every viewItemJobStep() call // The larger this value the less time we loose in checking the timeout every N messages. // On the other side, making this very large may make the view less responsive // if we're processing very few messages at a time and very high values (say > 10000) may // eventually make our job unbreakable until the end. int mMessageCheckCount; Pass mCurrentPass; // If this parameter is true then this job uses a "disconnected" UI. // It's FAR faster since we don't need to call beginInsertRows()/endInsertRows() // and we simply Q_EMIT a layoutChanged() at the end. It can be done only as the first // job though: subsequent jobs can't use layoutChanged() as it looses the expanded // state of items. bool mDisconnectUI; public: /** * Creates a "View Fill" operation job */ ViewItemJob(int startIndex, int endIndex, int chunkTimeout, int idleInterval, int messageCheckCount, bool disconnectUI = false) : mStartIndex(startIndex) , mCurrentIndex(startIndex) , mEndIndex(endIndex) , mInvariantIndexList(nullptr) , mChunkTimeout(chunkTimeout) , mIdleInterval(idleInterval) , mMessageCheckCount(messageCheckCount) , mCurrentPass(Pass1Fill) , mDisconnectUI(disconnectUI) { } /** * Creates a "View Cleanup" or "View Update" operation job */ ViewItemJob(Pass pass, QList< ModelInvariantIndex * > *invariantIndexList, int chunkTimeout, int idleInterval, int messageCheckCount) : mStartIndex(0) , mCurrentIndex(0) , mEndIndex(invariantIndexList->count() - 1) , mInvariantIndexList(invariantIndexList) , mChunkTimeout(chunkTimeout) , mIdleInterval(idleInterval) , mMessageCheckCount(messageCheckCount) , mCurrentPass(pass) , mDisconnectUI(false) { } ~ViewItemJob() { delete mInvariantIndexList; } public: int startIndex() const { return mStartIndex; } void setStartIndex(int startIndex) { mStartIndex = startIndex; mCurrentIndex = startIndex; } int currentIndex() const { return mCurrentIndex; } void setCurrentIndex(int currentIndex) { mCurrentIndex = currentIndex; } int endIndex() const { return mEndIndex; } void setEndIndex(int endIndex) { mEndIndex = endIndex; } Pass currentPass() const { return mCurrentPass; } void setCurrentPass(Pass pass) { mCurrentPass = pass; } int idleInterval() const { return mIdleInterval; } int chunkTimeout() const { return mChunkTimeout; } int messageCheckCount() const { return mMessageCheckCount; } QList< ModelInvariantIndex * > *invariantIndexList() const { return mInvariantIndexList; } bool disconnectUI() const { return mDisconnectUI; } }; } // namespace Core } // namespace MessageList using namespace MessageList::Core; Model::Model(View *pParent) : QAbstractItemModel(pParent) , d(new ModelPrivate(this)) { d->mRecursionCounterForReset = 0; d->mStorageModel = nullptr; d->mView = pParent; d->mAggregation = nullptr; d->mTheme = nullptr; d->mSortOrder = nullptr; d->mFilter = nullptr; d->mPersistentSetManager = nullptr; d->mInLengthyJobBatch = false; d->mLastSelectedMessageInFolder = nullptr; d->mLoading = false; d->mRootItem = new Item(Item::InvisibleRoot); d->mRootItem->setViewable(nullptr, true); d->mFillStepTimer.setSingleShot(true); d->mInvariantRowMapper = new ModelInvariantRowMapper(); d->mModelForItemFunctions = this; connect(&d->mFillStepTimer, &QTimer::timeout, this, [this]() { d->viewItemJobStep(); }); d->mCachedTodayLabel = i18n("Today"); d->mCachedYesterdayLabel = i18n("Yesterday"); d->mCachedUnknownLabel = i18nc("Unknown date", "Unknown"); d->mCachedLastWeekLabel = i18n("Last Week"); d->mCachedTwoWeeksAgoLabel = i18n("Two Weeks Ago"); d->mCachedThreeWeeksAgoLabel = i18n("Three Weeks Ago"); d->mCachedFourWeeksAgoLabel = i18n("Four Weeks Ago"); d->mCachedFiveWeeksAgoLabel = i18n("Five Weeks Ago"); d->mCachedWatchedOrIgnoredStatusBits = Akonadi::MessageStatus::statusIgnored().toQInt32() | Akonadi::MessageStatus::statusWatched().toQInt32(); connect(_k_heartBeatTimer(), &QTimer::timeout, this, [this]() { d->checkIfDateChanged(); }); if (!_k_heartBeatTimer->isActive()) { // First model starts it _k_heartBeatTimer->start(60000); // 1 minute } } Model::~Model() { setStorageModel(nullptr); d->clearJobList(); d->mOldestItem = nullptr; d->mNewestItem = nullptr; d->clearUnassignedMessageLists(); d->clearOrphanChildrenHash(); d->clearThreadingCacheReferencesIdMD5ToMessageItem(); d->clearThreadingCacheMessageSubjectMD5ToMessageItem(); delete d->mPersistentSetManager; // Delete the invariant row mapper before removing the items. // It's faster since the items will not need to call the invariant delete d->mInvariantRowMapper; delete d->mRootItem; delete d; } void Model::setAggregation(const Aggregation *aggregation) { d->mAggregation = aggregation; d->mView->setRootIsDecorated((d->mAggregation->grouping() == Aggregation::NoGrouping) && (d->mAggregation->threading() != Aggregation::NoThreading)); } void Model::setTheme(const Theme *theme) { d->mTheme = theme; } void Model::setSortOrder(const SortOrder *sortOrder) { d->mSortOrder = sortOrder; } const SortOrder *Model::sortOrder() const { return d->mSortOrder; } void Model::setFilter(const Filter *filter) { d->mFilter = filter; if (d->mFilter) { connect(d->mFilter, &Filter::finished, this, [this]() { d->slotApplyFilter(); }); } d->slotApplyFilter(); } void ModelPrivate::slotApplyFilter() { auto childList = mRootItem->childItems(); if (!childList) { return; } QModelIndex idx; // invalid QApplication::setOverrideCursor(Qt::WaitCursor); for (const auto child : qAsConst(*childList)) { applyFilterToSubtree(child, idx); } QApplication::restoreOverrideCursor(); } bool ModelPrivate::applyFilterToSubtree(Item *item, const QModelIndex &parentIndex) { // This function applies the current filter (eventually empty) // to a message tree starting at "item". if (!mModelForItemFunctions) { qCWarning(MESSAGELIST_LOG) << "Cannot apply filter, the UI must be not disconnected."; return true; } Q_ASSERT(item); // the item must obviously be valid Q_ASSERT(item->isViewable()); // the item must be viewable // Apply to children first auto childList = item->childItems(); bool childrenMatch = false; QModelIndex thisIndex = q->index(item, 0); if (childList) { for (const auto child : qAsConst(*childList)) { if (applyFilterToSubtree(child, thisIndex)) { childrenMatch = true; } } } if (!mFilter) { // empty filter always matches (but does not expand items) mView->setRowHidden(thisIndex.row(), parentIndex, false); return true; } if (childrenMatch) { mView->setRowHidden(thisIndex.row(), parentIndex, false); if (!mView->isExpanded(thisIndex)) { mView->expand(thisIndex); } return true; } if (item->type() == Item::Message) { if (mFilter->match((MessageItem *)item)) { mView->setRowHidden(thisIndex.row(), parentIndex, false); return true; } } // else this is a group header and it never explicitly matches // filter doesn't match, hide the item mView->setRowHidden(thisIndex.row(), parentIndex, true); return false; } int Model::columnCount(const QModelIndex &parent) const { if (!d->mTheme) { return 0; } if (parent.column() > 0) { return 0; } return d->mTheme->columns().count(); } QVariant Model::data(const QModelIndex &index, int role) const { /// this is called only when Akonadi is using the selectionmodel /// for item actions. since akonadi uses the ETM ItemRoles, and the /// messagelist uses its own internal roles, here we respond /// to the ETM ones. auto item = static_cast(index.internalPointer()); switch (role) { /// taken from entitytreemodel.h case Qt::UserRole + 1: //EntityTreeModel::ItemIdRole if (item->type() == MessageList::Core::Item::Message) { auto mItem = static_cast(item); return QVariant::fromValue(mItem->akonadiItem().id()); } else { return QVariant(); } break; case Qt::UserRole + 2: //EntityTreeModel::ItemRole if (item->type() == MessageList::Core::Item::Message) { auto mItem = static_cast(item); return QVariant::fromValue(mItem->akonadiItem()); } else { return QVariant(); } break; case Qt::UserRole + 3: //EntityTreeModel::MimeTypeRole if (item->type() == MessageList::Core::Item::Message) { return QStringLiteral("message/rfc822"); } else { return QVariant(); } break; case Qt::AccessibleTextRole: if (item->type() == MessageList::Core::Item::Message) { auto mItem = static_cast(item); return mItem->accessibleText(d->mTheme, index.column()); } else if (item->type() == MessageList::Core::Item::GroupHeader) { if (index.column() > 0) { return QString(); } auto hItem = static_cast(item); return hItem->label(); } return QString(); break; default: return QVariant(); } } QVariant Model::headerData(int section, Qt::Orientation, int role) const { if (!d->mTheme) { return QVariant(); } auto column = d->mTheme->column(section); if (!column) { return QVariant(); } if (d->mStorageModel && column->isSenderOrReceiver() && (role == Qt::DisplayRole)) { if (d->mStorageModelContainsOutboundMessages) { return QVariant(i18n("Receiver")); } return QVariant(i18n("Sender")); } const bool columnPixmapEmpty(column->pixmapName().isEmpty()); if ((role == Qt::DisplayRole) && columnPixmapEmpty) { return QVariant(column->label()); } else if ((role == Qt::ToolTipRole) && !columnPixmapEmpty) { return QVariant(column->label()); } else if ((role == Qt::DecorationRole) && !columnPixmapEmpty) { return QVariant(QIcon::fromTheme(column->pixmapName())); } return QVariant(); } QModelIndex Model::index(Item *item, int column) const { if (!d->mModelForItemFunctions) { return QModelIndex(); // called with disconnected UI: the item isn't known on the Qt side, yet } if (!item) { return QModelIndex(); } // FIXME: This function is a bottleneck (the caching in indexOfChildItem only works 30% of the time) auto par = item->parent(); if (!par) { if (item != d->mRootItem) { item->dump(QString()); } return QModelIndex(); } const int index = par->indexOfChildItem(item); if (index < 0) { return QModelIndex(); // BUG } return createIndex(index, column, item); } QModelIndex Model::index(int row, int column, const QModelIndex &parent) const { if (!d->mModelForItemFunctions) { return QModelIndex(); // called with disconnected UI: the item isn't known on the Qt side, yet } #ifdef READD_THIS_IF_YOU_WANT_TO_PASS_MODEL_TEST if (column < 0) { return QModelIndex(); // senseless column (we could optimize by skipping this check but ModelTest from trolltech is pedantic) } #endif const Item *item; if (parent.isValid()) { item = static_cast(parent.internalPointer()); if (!item) { return QModelIndex(); // should never happen } } else { item = d->mRootItem; } if (parent.column() > 0) { return QModelIndex(); // parent column is not 0: shouldn't have children (as per Qt documentation) } Item *child = item->childItem(row); if (!child) { return QModelIndex(); // no such row in parent } return createIndex(row, column, child); } QModelIndex Model::parent(const QModelIndex &modelIndex) const { Q_ASSERT(d->mModelForItemFunctions); // should be never called with disconnected UI if (!modelIndex.isValid()) { return QModelIndex(); // should never happen } auto item = static_cast(modelIndex.internalPointer()); if (!item) { return QModelIndex(); } auto par = item->parent(); if (!par) { return QModelIndex(); // should never happen } //return index( par, modelIndex.column() ); return index(par, 0); // parents are always in column 0 (as per Qt documentation) } int Model::rowCount(const QModelIndex &parent) const { if (!d->mModelForItemFunctions) { return 0; // called with disconnected UI } const Item *item; if (parent.isValid()) { item = static_cast(parent.internalPointer()); if (!item) { return 0; // should never happen } } else { item = d->mRootItem; } if (!item->isViewable()) { return 0; } return item->childItemCount(); } class RecursionPreventer { public: RecursionPreventer(int &counter) : mCounter(counter) { mCounter++; } ~RecursionPreventer() { mCounter--; } bool isRecursive() const { return mCounter > 1; } private: int &mCounter; }; StorageModel *Model::storageModel() const { return d->mStorageModel; } void ModelPrivate::clear() { q->beginResetModel(); if (mFillStepTimer.isActive()) { mFillStepTimer.stop(); } // Kill pre-selection at this stage mPreSelectionMode = PreSelectNone; mLastSelectedMessageInFolder = nullptr; mOldestItem = nullptr; mNewestItem = nullptr; // Reset the row mapper before removing items // This is faster since the items don't need to access the mapper. mInvariantRowMapper->modelReset(); clearJobList(); clearUnassignedMessageLists(); clearOrphanChildrenHash(); mGroupHeaderItemHash.clear(); mGroupHeadersThatNeedUpdate.clear(); mThreadingCacheMessageIdMD5ToMessageItem.clear(); mThreadingCacheMessageInReplyToIdMD5ToMessageItem.clear(); clearThreadingCacheReferencesIdMD5ToMessageItem(); clearThreadingCacheMessageSubjectMD5ToMessageItem(); mViewItemJobStepChunkTimeout = 100; mViewItemJobStepIdleInterval = 10; mViewItemJobStepMessageCheckCount = 10; delete mPersistentSetManager; mPersistentSetManager = nullptr; mCurrentItemToRestoreAfterViewItemJobStep = nullptr; mTodayDate = QDate::currentDate(); // FIXME: CLEAR THE FILTER HERE AS WE CAN'T APPLY IT WITH UI DISCONNECTED! mRootItem->killAllChildItems(); q->endResetModel(); //Q_EMIT headerDataChanged(); mView->selectionModel()->clearSelection(); } void Model::setStorageModel(StorageModel *storageModel, PreSelectionMode preSelectionMode) { // Prevent a case of recursion when opening a folder that has a message and the folder was // never opened before. RecursionPreventer preventer(d->mRecursionCounterForReset); if (preventer.isRecursive()) { return; } d->clear(); if (d->mStorageModel) { // Disconnect all signals from old storageModel std::for_each(d->mStorageModelConnections.cbegin(), d->mStorageModelConnections.cend(), [](const QMetaObject::Connection &c) -> bool { return QObject::disconnect(c); }); d->mStorageModelConnections.clear(); } const bool isReload = (d->mStorageModel == storageModel); d->mStorageModel = storageModel; if (!d->mStorageModel) { return; // no folder: nothing to fill } // Save threading cache of the previous folder, but only if the cache was // enabled and a different folder is being loaded - reload of the same folder // means change in aggregation in which case we will have to re-build the // cache so there's no point saving the current threading cache. if (d->mThreadingCache.isEnabled() && !isReload) { d->mThreadingCache.save(); } else { if (isReload) { qCDebug(MESSAGELIST_LOG) << "Identical folder reloaded, not saving old threading cache"; } else { qCDebug(MESSAGELIST_LOG) << "Threading disabled in previous folder, not saving threading cache"; } } // Load threading cache for the new folder, but only if threading is enabled, // otherwise we would just be caching a flat list. if (d->mAggregation->threading() != Aggregation::NoThreading) { d->mThreadingCache.setEnabled(true); d->mThreadingCache.load(d->mStorageModel->id(), d->mAggregation); } else { // No threading, no cache - don't even bother inserting entries into the // cache or trying to look them up there d->mThreadingCache.setEnabled(false); qCDebug(MESSAGELIST_LOG) << "Threading disabled in folder" << d->mStorageModel->id() << ", not using threading cache"; } d->mPreSelectionMode = preSelectionMode; d->mStorageModelContainsOutboundMessages = d->mStorageModel->containsOutboundMessages(); d->mStorageModelConnections = { connect(d->mStorageModel, &StorageModel::rowsInserted, this, [this](const QModelIndex &parent, int first, int last) { d->slotStorageModelRowsInserted(parent, first, last); }), connect(d->mStorageModel, &StorageModel::rowsRemoved, this, [this](const QModelIndex &parent, int first, int last) { d->slotStorageModelRowsRemoved(parent, first, last); }), connect(d->mStorageModel, &StorageModel::layoutChanged, this, [this]() { d->slotStorageModelLayoutChanged(); }), connect(d->mStorageModel, &StorageModel::modelReset, this, [this]() { d->slotStorageModelLayoutChanged(); }), connect(d->mStorageModel, &StorageModel::dataChanged, this, [this](const QModelIndex &topLeft, const QModelIndex &bottomRight) { d->slotStorageModelDataChanged(topLeft, bottomRight); }), connect(d->mStorageModel, &StorageModel::headerDataChanged, this, [this](Qt::Orientation orientation, int first, int last) { d->slotStorageModelHeaderDataChanged(orientation, first, last); }) }; if (d->mStorageModel->rowCount() == 0) { return; // folder empty: nothing to fill } // Here we use different strategies based on user preference and the folder size. // The knobs we can tune are: // // - The number of jobs used to scan the whole folder and their order // // There are basically two approaches to this. One is the "single big job" // approach. It scans the folder from the beginning to the end in a single job // entry. The job passes are done only once. It's advantage is that it's simplier // and it's less likely to generate imperfect parent threadings. The bad // side is that since the folders are "sort of" date ordered then the most interesting // messages show up at the end of the work. Not nice for large folders. // The other approach uses two jobs. This is a bit slower but smarter strategy. // First we scan the latest 1000 messages and *then* take care of the older ones. // This will show up the most interesting messages almost immediately. (Well... // All this assuming that the underlying storage always appends the newly arrived messages) // The strategy is slower since it generates some imperfect parent threadings which must be // adjusted by the second job. For instance, in my kernel mailing list folder this "smart" approach // generates about 150 additional imperfectly threaded children... but the "today" // messages show up almost immediately. The two-chunk job also makes computing // the percentage user feedback a little harder and might break some optimization // in the insertions (we're able to optimize appends and prepends but a chunked // job is likely to split our work at a boundary where messages are always inserted // in the middle of the list). // // - The maximum time to spend inside a single job step // // The larger this time, the greater the number of messages per second that this // engine can process but also greater time with frozen UI -> less interactivity. // Reasonable values start at 50 msecs. Values larger than 300 msecs are very likely // to be perceived by the user as UI non-reactivity. // // - The number of messages processed in each job step subchunk. // // A job subchunk is processed without checking the maximum time above. This means // that each job step will process at least the number of messages specified by this value. // Very low values mean that we respect the maximum time very carefully but we also // waste time to check if we ran out of time :) // Very high values are likely to cause the engine to not respect the maximum step time. // Reasonable values go from 5 to 100. // // - The "idle" time between two steps // // The lower this time, the greater the number of messages per second that this // engine can process but also lower time for the UI to process events -> less interactivity. // A value of 0 here means that Qt will trigger the timer as soon as it has some // idle time to spend. UI events will be still processed but slowdowns are possible. // 0 is reasonable though. Values larger than 200 will tend to make the total job // completion times high. // // If we have no filter it seems that we can apply a huge optimization. // We disconnect the UI for the first huge filling job. This allows us // to save the extremely expensive beginInsertRows()/endInsertRows() calls // and call a single layoutChanged() at the end. This slows down a lot item // expansion. But on the other side if only few items need to be expanded // then this strategy is better. If filtering is enabled then this strategy // isn't applicable (because filtering requires interaction with the UI // while the data is loading). // So... // For the very first small chunk it's ok to work with disconnected UI as long // as we have no filter. The first small chunk is always 1000 messages, so // even if all of them are expanded, it's still somewhat acceptable. bool canDoFirstSmallChunkWithDisconnectedUI = !d->mFilter; // Larger works need a bigger condition: few messages must be expanded in the end. bool canDoJobWithDisconnectedUI =// we have no filter !d->mFilter && ( // we do no threading at all (d->mAggregation->threading() == Aggregation::NoThreading) || // or we never expand threads (d->mAggregation->threadExpandPolicy() == Aggregation::NeverExpandThreads) || // or we expand threads but we'll be going to expand really only a few ( // so we don't expand them all (d->mAggregation->threadExpandPolicy() != Aggregation::AlwaysExpandThreads) && // and we'd expand only a few in fact (d->mStorageModel->initialUnreadRowCountGuess() < 1000) ) ); switch (d->mAggregation->fillViewStrategy()) { case Aggregation::FavorInteractivity: // favor interactivity if ((!canDoJobWithDisconnectedUI) && (d->mStorageModel->rowCount() > 3000)) { // empiric value // First a small job with the most recent messages. Large chunk, small (but non zero) idle interval // and a larger number of messages to process at once. auto job1 = new ViewItemJob(d->mStorageModel->rowCount() - 1000, d->mStorageModel->rowCount() - 1, 200, 20, 100, canDoFirstSmallChunkWithDisconnectedUI); d->mViewItemJobs.append(job1); // Then a larger job with older messages. Small chunk, bigger idle interval, small number of messages to // process at once. auto job2 = new ViewItemJob(0, d->mStorageModel->rowCount() - 1001, 100, 50, 10, false); d->mViewItemJobs.append(job2); // We could even extremize this by splitting the folder in several // chunks and scanning them from the newest to the oldest... but the overhead // due to imperfectly threaded children would be probably too big. } else { // small folder or can be done with disconnected UI: single chunk work. // Lag the CPU a bit more but not too much to destroy even the earliest interactivity. auto job = new ViewItemJob(0, d->mStorageModel->rowCount() - 1, 150, 30, 30, canDoJobWithDisconnectedUI); d->mViewItemJobs.append(job); } break; case Aggregation::FavorSpeed: // More batchy jobs, still interactive to a certain degree if ((!canDoJobWithDisconnectedUI) && (d->mStorageModel->rowCount() > 3000)) { // empiric value // large folder, but favor speed auto job1 = new ViewItemJob(d->mStorageModel->rowCount() - 1000, d->mStorageModel->rowCount() - 1, 250, 0, 100, canDoFirstSmallChunkWithDisconnectedUI); d->mViewItemJobs.append(job1); auto job2 = new ViewItemJob(0, d->mStorageModel->rowCount() - 1001, 200, 0, 10, false); d->mViewItemJobs.append(job2); } else { // small folder or can be done with disconnected UI and favor speed: single chunk work. // Lag the CPU more, get more work done auto job = new ViewItemJob(0, d->mStorageModel->rowCount() - 1, 250, 0, 100, canDoJobWithDisconnectedUI); d->mViewItemJobs.append(job); } break; case Aggregation::BatchNoInteractivity: { // one large job, never interrupt, block UI auto job = new ViewItemJob(0, d->mStorageModel->rowCount() - 1, 60000, 0, 100000, canDoJobWithDisconnectedUI); d->mViewItemJobs.append(job); break; } default: qCWarning(MESSAGELIST_LOG) << "Unrecognized fill view strategy"; Q_ASSERT(false); break; } d->mLoading = true; d->viewItemJobStep(); } void ModelPrivate::checkIfDateChanged() { // This function is called by MessageList::Core::Manager once in a while (every 1 minute or sth). // It is used to check if the current date has changed (with respect to mTodayDate). // // Our message items cache the formatted dates (as formatting them // on the fly would be too expensive). We also cache the labels of the groups which often display dates. // When the date changes we would need to fix all these strings. // // A dedicated algorithm to refresh the labels of the items would be either too complex // or would block on large trees. Fixing the labels of the groups is also quite hard... // // So to keep the things simple we just reload the view. if (!mStorageModel) { return; // nothing to do } if (mLoading) { return; // not now } if (!mViewItemJobs.isEmpty()) { return; // not now } if (mTodayDate == QDate::currentDate()) { return; // date not changed } // date changed, reload the view (and try to preserve the current selection) q->setStorageModel(mStorageModel, PreSelectLastSelected); } void Model::setPreSelectionMode(PreSelectionMode preSelect) { d->mPreSelectionMode = preSelect; d->mLastSelectedMessageInFolder = nullptr; } // // The "view fill" algorithm implemented in the functions below is quite smart but also quite complex. // It's governed by the following goals: // // - Be flexible: allow different configurations from "unsorted flat list" to a "grouped and threaded // list with different sorting algorithms applied to each aggregation level" // - Be reasonably fast // - Be non blocking: UI shouldn't freeze while the algorithm is running // - Be interruptible: user must be able to abort the execution and just switch to another folder in the middle // void ModelPrivate::clearUnassignedMessageLists() { // This is a bit tricky... // The three unassigned message lists contain messages that have been created // but not yet attached to the view. There may be two major cases for a message: // - it has no parent -> it must be deleted and it will delete its children too // - it has a parent -> it must NOT be deleted since it will be deleted by its parent. // Sometimes the things get a little complicated since in Pass2 and Pass3 // we have transitional states in that the MessageItem object can be in two of these lists. // WARNING: This function does NOT fixup mNewestItem and mOldestItem. If one of these // two messages is in the lists below, it's deleted and the member becomes a dangling pointer. // The caller must ensure that both mNewestItem and mOldestItem are set to 0 // and this is enforced in the assert below to avoid errors. This basically means // that this function should be called only when the storage model changes or // when the model is destroyed. Q_ASSERT((mOldestItem == nullptr) && (mNewestItem == nullptr)); if (!mUnassignedMessageListForPass2.isEmpty()) { // We're actually in Pass1* or Pass2: everything is mUnassignedMessageListForPass2 // Something may *also* be in mUnassignedMessageListForPass3 and mUnassignedMessageListForPass4 // but that are duplicates for sure. // We can't just sweep the list and delete parentless items since each delete // could kill children which are somewhere AFTER in the list: accessing the children // would then lead to a SIGSEGV. We first sweep the list gathering parentless // items and *then* delete them without accessing the parented ones. QList parentless; for (const auto mi : qAsConst(mUnassignedMessageListForPass2)) { if (!mi->parent()) { parentless.append(mi); } } for (const auto mi : qAsConst(parentless)) { delete mi; } mUnassignedMessageListForPass2.clear(); // Any message these list contain was also in mUnassignedMessageListForPass2 mUnassignedMessageListForPass3.clear(); mUnassignedMessageListForPass4.clear(); return; } // mUnassignedMessageListForPass2 is empty if (!mUnassignedMessageListForPass3.isEmpty()) { // We're actually at the very end of Pass2 or inside Pass3 // Pass2 pushes stuff in mUnassignedMessageListForPass3 *or* mUnassignedMessageListForPass4 // Pass3 pushes stuff from mUnassignedMessageListForPass3 to mUnassignedMessageListForPass4 // So if we're in Pass2 then the two lists contain distinct messages but if we're in Pass3 // then the two lists may contain the same messages. if (!mUnassignedMessageListForPass4.isEmpty()) { // We're actually in Pass3: the messiest one. QSet itemsToDelete; for (const auto mi : qAsConst(mUnassignedMessageListForPass3)) { if (!mi->parent()) { itemsToDelete.insert(mi); } } for (const auto mi : qAsConst(mUnassignedMessageListForPass4)) { if (!mi->parent()) { itemsToDelete.insert(mi); } } for (const auto mi : qAsConst(itemsToDelete)) { delete mi; } mUnassignedMessageListForPass3.clear(); mUnassignedMessageListForPass4.clear(); return; } // mUnassignedMessageListForPass4 is empty so we must be at the end of a very special kind of Pass2 // We have the same problem as in mUnassignedMessageListForPass2. QList parentless; for (const auto mi : qAsConst(mUnassignedMessageListForPass3)) { if (!mi->parent()) { parentless.append(mi); } } for (const auto mi : qAsConst(parentless)) { delete mi; } mUnassignedMessageListForPass3.clear(); return; } // mUnassignedMessageListForPass3 is empty if (!mUnassignedMessageListForPass4.isEmpty()) { // we're in Pass4.. this is easy. // We have the same problem as in mUnassignedMessageListForPass2. QList parentless; for (const auto mi : qAsConst(mUnassignedMessageListForPass4)) { if (!mi->parent()) { parentless.append(mi); } } for (const auto mi : qAsConst(parentless)) { delete mi; } mUnassignedMessageListForPass4.clear(); return; } } void ModelPrivate::clearThreadingCacheReferencesIdMD5ToMessageItem() { qDeleteAll(mThreadingCacheMessageReferencesIdMD5ToMessageItem); mThreadingCacheMessageReferencesIdMD5ToMessageItem.clear(); } void ModelPrivate::clearThreadingCacheMessageSubjectMD5ToMessageItem() { qDeleteAll(mThreadingCacheMessageSubjectMD5ToMessageItem); mThreadingCacheMessageSubjectMD5ToMessageItem.clear(); } void ModelPrivate::clearOrphanChildrenHash() { qDeleteAll(mOrphanChildrenHash); mOrphanChildrenHash.clear(); } void ModelPrivate::clearJobList() { if (mViewItemJobs.isEmpty()) { return; } if (mInLengthyJobBatch) { mInLengthyJobBatch = false; } qDeleteAll(mViewItemJobs); mViewItemJobs.clear(); mModelForItemFunctions = q; // make sure it's true, as there remains no job with disconnected UI } void ModelPrivate::attachGroup(GroupHeaderItem *ghi) { if (ghi->parent()) { if ( ((ghi)->childItemCount() > 0) // has children && (ghi)->isViewable() // is actually attached to the viewable root && mModelForItemFunctions // the UI is not disconnected && mView->isExpanded(q->index(ghi, 0)) // is actually expanded ) { saveExpandedStateOfSubtree(ghi); } // FIXME: This *WILL* break selection and current index... :/ ghi->parent()->takeChildItem(mModelForItemFunctions, ghi); } ghi->setParent(mRootItem); // I'm using a macro since it does really improve readability. // I'm NOT using a helper function since gcc will refuse to inline some of // the calls because they make this function grow too much. #define INSERT_GROUP_WITH_COMPARATOR(_ItemComparator) \ switch (mSortOrder->groupSortDirection()) \ { \ case SortOrder::Ascending: \ mRootItem->d_ptr->insertChildItem< _ItemComparator, true >(mModelForItemFunctions, ghi); \ break; \ case SortOrder::Descending: \ mRootItem->d_ptr->insertChildItem< _ItemComparator, false >(mModelForItemFunctions, ghi); \ break; \ default: /* should never happen... */ \ mRootItem->appendChildItem(mModelForItemFunctions, ghi); \ break; \ } switch (mSortOrder->groupSorting()) { case SortOrder::SortGroupsByDateTime: INSERT_GROUP_WITH_COMPARATOR(ItemDateComparator) break; case SortOrder::SortGroupsByDateTimeOfMostRecent: INSERT_GROUP_WITH_COMPARATOR(ItemMaxDateComparator) break; case SortOrder::SortGroupsBySenderOrReceiver: INSERT_GROUP_WITH_COMPARATOR(ItemSenderOrReceiverComparator) break; case SortOrder::SortGroupsBySender: INSERT_GROUP_WITH_COMPARATOR(ItemSenderComparator) break; case SortOrder::SortGroupsByReceiver: INSERT_GROUP_WITH_COMPARATOR(ItemReceiverComparator) break; case SortOrder::NoGroupSorting: mRootItem->appendChildItem(mModelForItemFunctions, ghi); break; default: // should never happen mRootItem->appendChildItem(mModelForItemFunctions, ghi); break; } if (ghi->initialExpandStatus() == Item::ExpandNeeded) { // this actually is a "non viewable expanded state" if (ghi->childItemCount() > 0) { if (mModelForItemFunctions) { // the UI is not disconnected syncExpandedStateOfSubtree(ghi); } } } // A group header is always viewable, when attached: apply the filter, if we have it. if (mFilter) { Q_ASSERT(mModelForItemFunctions); // UI must be NOT disconnected // apply the filter to subtree applyFilterToSubtree(ghi, QModelIndex()); } } void ModelPrivate::saveExpandedStateOfSubtree(Item *root) { Q_ASSERT(mModelForItemFunctions); // UI must be NOT disconnected here Q_ASSERT(root); root->setInitialExpandStatus(Item::ExpandNeeded); auto children = root->childItems(); if (!children) { return; } for (const auto mi : qAsConst(*children)) { if (mi->childItemCount() > 0 // has children && mi->isViewable() // is actually attached to the viewable root && mView->isExpanded(q->index(mi, 0))) { // is actually expanded saveExpandedStateOfSubtree(mi); } } } void ModelPrivate::syncExpandedStateOfSubtree(Item *root) { Q_ASSERT(mModelForItemFunctions); // UI must be NOT disconnected here // WE ASSUME that: // - the item is viewable // - its initialExpandStatus() is Item::ExpandNeeded // - it has at least one children (well.. this is not a strict requirement, but it's a waste of resources to expand items that don't have children) QModelIndex idx = q->index(root, 0); //if ( !mView->isExpanded( idx ) ) // this is O(logN!) in Qt.... very ugly... but it should never happen here mView->expand(idx); // sync the real state in the view root->setInitialExpandStatus(Item::ExpandExecuted); auto children = root->childItems(); if (!children) { return; } for (const auto mi : qAsConst(*children)) { if (mi->initialExpandStatus() == Item::ExpandNeeded) { if (mi->childItemCount() > 0) { syncExpandedStateOfSubtree(mi); } } } } void ModelPrivate::attachMessageToGroupHeader(MessageItem *mi) { QString groupLabel; time_t date; // compute the group header label and the date switch (mAggregation->grouping()) { case Aggregation::GroupByDate: case Aggregation::GroupByDateRange: { if (mAggregation->threadLeader() == Aggregation::MostRecentMessage) { date = mi->maxDate(); } else { date = mi->date(); } QDateTime dt; dt.setSecsSinceEpoch(date); QDate dDate = dt.date(); int daysAgo = -1; const int daysInWeek = 7; if (dDate.isValid() && mTodayDate.isValid()) { daysAgo = dDate.daysTo(mTodayDate); } if ((daysAgo < 0) // In the future || (static_cast< uint >(date) == static_cast< uint >(-1))) { // Invalid groupLabel = mCachedUnknownLabel; } else if (daysAgo == 0) { // Today groupLabel = mCachedTodayLabel; } else if (daysAgo == 1) { // Yesterday groupLabel = mCachedYesterdayLabel; } else if (daysAgo > 1 && daysAgo < daysInWeek) { // Within last seven days auto dayName = mCachedDayNameLabel.find(dDate.dayOfWeek()); // non-const call, but non-shared container if (dayName == mCachedDayNameLabel.end()) { dayName = mCachedDayNameLabel.insert(dDate.dayOfWeek(), QLocale::system().standaloneDayName(dDate.dayOfWeek())); } groupLabel = *dayName; } else if (mAggregation->grouping() == Aggregation::GroupByDate) { // GroupByDate seven days or more ago groupLabel = QLocale::system().toString(dDate, QLocale::ShortFormat); } else if (dDate.month() == mTodayDate.month() // GroupByDateRange within this month && dDate.year() == mTodayDate.year()) { int startOfWeekDaysAgo = (daysInWeek + mTodayDate.dayOfWeek() - QLocale().firstDayOfWeek()) % daysInWeek; int weeksAgo = ((daysAgo - startOfWeekDaysAgo) / daysInWeek) + 1; switch (weeksAgo) { case 0: // This week groupLabel = QLocale::system().standaloneDayName(dDate.dayOfWeek()); break; case 1: // 1 week ago groupLabel = mCachedLastWeekLabel; break; case 2: groupLabel = mCachedTwoWeeksAgoLabel; break; case 3: groupLabel = mCachedThreeWeeksAgoLabel; break; case 4: groupLabel = mCachedFourWeeksAgoLabel; break; case 5: groupLabel = mCachedFiveWeeksAgoLabel; break; default: // should never happen groupLabel = mCachedUnknownLabel; } } else if (dDate.year() == mTodayDate.year()) { // GroupByDateRange within this year auto monthName = mCachedMonthNameLabel.find(dDate.month()); // non-const call, but non-shared container if (monthName == mCachedMonthNameLabel.end()) { monthName = mCachedMonthNameLabel.insert(dDate.month(), QLocale::system().standaloneMonthName(dDate.month())); } groupLabel = *monthName; } else { // GroupByDateRange in previous years auto monthName = mCachedMonthNameLabel.find(dDate.month()); // non-const call, but non-shared container if (monthName == mCachedMonthNameLabel.end()) { monthName = mCachedMonthNameLabel.insert(dDate.month(), QLocale::system().standaloneMonthName(dDate.month())); } groupLabel = i18nc("Message Aggregation Group Header: Month name and Year number", "%1 %2", *monthName, QLocale::system().toString(dDate, QLatin1String("yyyy"))); } break; } case Aggregation::GroupBySenderOrReceiver: date = mi->date(); groupLabel = mi->displaySenderOrReceiver(); break; case Aggregation::GroupBySender: date = mi->date(); groupLabel = mi->displaySender(); break; case Aggregation::GroupByReceiver: date = mi->date(); groupLabel = mi->displayReceiver(); break; case Aggregation::NoGrouping: // append directly to root attachMessageToParent(mRootItem, mi); return; default: // should never happen attachMessageToParent(mRootItem, mi); return; } GroupHeaderItem *ghi; ghi = mGroupHeaderItemHash.value(groupLabel, nullptr); if (!ghi) { // not found ghi = new GroupHeaderItem(groupLabel); ghi->initialSetup(date, mi->size(), mi->sender(), mi->receiver(), mi->useReceiver()); switch (mAggregation->groupExpandPolicy()) { case Aggregation::NeverExpandGroups: // nothing to do break; case Aggregation::AlwaysExpandGroups: // expand always ghi->setInitialExpandStatus(Item::ExpandNeeded); break; case Aggregation::ExpandRecentGroups: // expand only if "close" to today if (mViewItemJobStepStartTime > ghi->date()) { if ((mViewItemJobStepStartTime - ghi->date()) < (3600 * 72)) { ghi->setInitialExpandStatus(Item::ExpandNeeded); } } else { if ((ghi->date() - mViewItemJobStepStartTime) < (3600 * 72)) { ghi->setInitialExpandStatus(Item::ExpandNeeded); } } break; default: // b0rken break; } attachMessageToParent(ghi, mi); attachGroup(ghi); // this will expand the group if required mGroupHeaderItemHash.insert(groupLabel, ghi); } else { // the group was already there (certainly viewable) // This function may be also called to re-group a message. // That is, to eventually find a new group for a message that has changed // its properties (but was already attached to a group). // So it may happen that we find out that in fact re-grouping wasn't really // needed because the message is already in the correct group. if (mi->parent() == ghi) { return; // nothing to be done } attachMessageToParent(ghi, mi); } // Remember this message as a thread leader mThreadingCache.updateParent(mi, nullptr); } MessageItem *ModelPrivate::findMessageParent(MessageItem *mi) { Q_ASSERT(mAggregation->threading() != Aggregation::NoThreading); // caller must take care of this // This function attempts to find a thread parent for the item "mi" // which actually may already have a children subtree. // Forged or plain broken message trees are dangerous here. // For example, a message tree with circular references like // // Message mi, Id=1, In-Reply-To=2 // Message childOfMi, Id=2, In-Reply-To=1 // // is perfectly possible and will cause us to find childOfMi // as parent of mi. This will then create a loop in the message tree // (which will then no longer be a tree in fact) and cause us to freeze // once we attempt to climb the parents. We need to take care of that. bool bMessageWasThreadable = false; MessageItem *pParent; // First of all try to find a "perfect parent", that is the message for that // we have the ID in the "In-Reply-To" field. This is actually done by using // MD5 caches of the message ids because of speed. Collisions are very unlikely. QByteArray md5 = mi->inReplyToIdMD5(); if (!md5.isEmpty()) { // have an In-Reply-To field MD5 pParent = mThreadingCacheMessageIdMD5ToMessageItem.value(md5, nullptr); if (pParent) { // Take care of circular references if ( (mi == pParent) // self referencing message || ( (mi->childItemCount() > 0) // mi already has children, this is fast to determine && pParent->hasAncestor(mi) // pParent is in the mi's children tree ) ) { qCWarning(MESSAGELIST_LOG) << "Circular In-Reply-To reference loop detected in the message tree"; mi->setThreadingStatus(MessageItem::NonThreadable); return nullptr; // broken message: throw it away } mi->setThreadingStatus(MessageItem::PerfectParentFound); return pParent; // got a perfect parent for this message } // got no perfect parent bMessageWasThreadable = true; // but the message was threadable } if (mAggregation->threading() == Aggregation::PerfectOnly) { mi->setThreadingStatus(bMessageWasThreadable ? MessageItem::ParentMissing : MessageItem::NonThreadable); return nullptr; // we're doing only perfect parent matches } // Try to use the "References" field. In fact we have the MD5 of the // (n-1)th entry in References. // // Original rationale from KMHeaders: // // If we don't have a replyToId, or if we have one and the // corresponding message is not in this folder, as happens // if you keep your outgoing messages in an OUTBOX, for // example, try the list of references, because the second // to last will likely be in this folder. replyToAuxIdMD5 // contains the second to last one. md5 = mi->referencesIdMD5(); if (!md5.isEmpty()) { pParent = mThreadingCacheMessageIdMD5ToMessageItem.value(md5, nullptr); if (pParent) { // Take care of circular references if ( (mi == pParent) // self referencing message || ( (mi->childItemCount() > 0) // mi already has children, this is fast to determine && pParent->hasAncestor(mi) // pParent is in the mi's children tree ) ) { qCWarning(MESSAGELIST_LOG) << "Circular reference loop detected in the message tree"; mi->setThreadingStatus(MessageItem::NonThreadable); return nullptr; // broken message: throw it away } mi->setThreadingStatus(MessageItem::ImperfectParentFound); return pParent; // got an imperfect parent for this message } auto messagesWithTheSameReferences = mThreadingCacheMessageReferencesIdMD5ToMessageItem.value(md5, nullptr); if (messagesWithTheSameReferences) { Q_ASSERT(!messagesWithTheSameReferences->isEmpty()); pParent = messagesWithTheSameReferences->first(); if (mi != pParent && (mi->childItemCount() == 0 || !pParent->hasAncestor(mi))) { mi->setThreadingStatus(MessageItem::ImperfectParentFound); return pParent; } } // got no imperfect parent bMessageWasThreadable = true; // but the message was threadable } if (mAggregation->threading() == Aggregation::PerfectAndReferences) { mi->setThreadingStatus(bMessageWasThreadable ? MessageItem::ParentMissing : MessageItem::NonThreadable); return nullptr; // we're doing only perfect parent matches } Q_ASSERT(mAggregation->threading() == Aggregation::PerfectReferencesAndSubject); // We are supposed to do subject based threading but we can't do it now. // This is because the subject based threading *may* be wrong and waste // time by creating circular references (that we'd need to detect and fix). // We first try the perfect and references based threading on all the messages // and then run subject based threading only on the remaining ones. mi->setThreadingStatus((bMessageWasThreadable || mi->subjectIsPrefixed()) ? MessageItem::ParentMissing : MessageItem::NonThreadable); return nullptr; } // Subject threading cache stuff #if 0 // Debug helpers void dump_iterator_and_list(QList< MessageItem * >::Iterator &iter, QList< MessageItem * > *list) { qCDebug(MESSAGELIST_LOG) << "Threading cache part dump"; if (iter == list->end()) { qCDebug(MESSAGELIST_LOG) << "Iterator pointing to end of the list"; } else { qCDebug(MESSAGELIST_LOG) << "Iterator pointing to " << *iter << " subject [" << (*iter)->subject() << "] date [" << (*iter)->date() << "]"; } for (QList< MessageItem * >::Iterator it = list->begin(); it != list->end(); ++it) { qCDebug(MESSAGELIST_LOG) << "List element " << *it << " subject [" << (*it)->subject() << "] date [" << (*it)->date() << "]"; } qCDebug(MESSAGELIST_LOG) << "End of threading cache part dump"; } void dump_list(QList< MessageItem * > *list) { qCDebug(MESSAGELIST_LOG) << "Threading cache part dump"; for (QList< MessageItem * >::Iterator it = list->begin(); it != list->end(); ++it) { qCDebug(MESSAGELIST_LOG) << "List element " << *it << " subject [" << (*it)->subject() << "] date [" << (*it)->date() << "]"; } qCDebug(MESSAGELIST_LOG) << "End of threading cache part dump"; } #endif // debug helpers // a helper class used in a qLowerBound() call below class MessageLessThanByDate { public: inline bool operator()(const MessageItem *mi1, const MessageItem *mi2) const { if (mi1->date() < mi2->date()) { // likely return true; } if (mi1->date() > mi2->date()) { // likely return false; } // dates are equal, compare by pointer return mi1 < mi2; } }; void ModelPrivate::addMessageToReferencesBasedThreadingCache(MessageItem *mi) { // Messages in this cache are sorted by date, and if dates are equal then they are sorted by pointer value. // Sorting by date is used to optimize the parent lookup in guessMessageParent() below. // WARNING: If the message date changes for some reason (like in the "update" step) // then the cache may become unsorted. For this reason the message about to // be changed must be first removed from the cache and then reinserted. auto messagesWithTheSameReference = mThreadingCacheMessageReferencesIdMD5ToMessageItem.value(mi->referencesIdMD5(), nullptr); if (!messagesWithTheSameReference) { messagesWithTheSameReference = new QList< MessageItem * >(); mThreadingCacheMessageReferencesIdMD5ToMessageItem.insert(mi->referencesIdMD5(), messagesWithTheSameReference); messagesWithTheSameReference->append(mi); return; } // Found: assert that we have no duplicates in the cache. Q_ASSERT(!messagesWithTheSameReference->contains(mi)); // Ordered insert: first by date then by pointer value. auto it = std::lower_bound(messagesWithTheSameReference->begin(), messagesWithTheSameReference->end(), mi, MessageLessThanByDate()); messagesWithTheSameReference->insert(it, mi); } void ModelPrivate::removeMessageFromReferencesBasedThreadingCache(MessageItem *mi) { // We assume that the caller knows what he is doing and the message is actually in the cache. // If the message isn't in the cache then we should not be called at all. auto messagesWithTheSameReference = mThreadingCacheMessageReferencesIdMD5ToMessageItem.value(mi->referencesIdMD5(), nullptr); // We assume that the message is there so the list must be non null. Q_ASSERT(messagesWithTheSameReference); // The cache *MUST* be ordered first by date then by pointer value auto it = std::lower_bound(messagesWithTheSameReference->begin(), messagesWithTheSameReference->end(), mi, MessageLessThanByDate()); // The binary based search must have found a message Q_ASSERT(it != messagesWithTheSameReference->end()); // and it must have found exactly the message requested Q_ASSERT(*it == mi); // Kill it messagesWithTheSameReference->erase(it); // And kill the list if it was the last one if (messagesWithTheSameReference->isEmpty()) { mThreadingCacheMessageReferencesIdMD5ToMessageItem.remove(mi->referencesIdMD5()); delete messagesWithTheSameReference; } } void ModelPrivate::addMessageToSubjectBasedThreadingCache(MessageItem *mi) { // Messages in this cache are sorted by date, and if dates are equal then they are sorted by pointer value. // Sorting by date is used to optimize the parent lookup in guessMessageParent() below. // WARNING: If the message date changes for some reason (like in the "update" step) // then the cache may become unsorted. For this reason the message about to // be changed must be first removed from the cache and then reinserted. // Lookup the list of messages with the same stripped subject auto messagesWithTheSameStrippedSubject = mThreadingCacheMessageSubjectMD5ToMessageItem.value(mi->strippedSubjectMD5(), nullptr); if (!messagesWithTheSameStrippedSubject) { // Not there yet: create it and append. messagesWithTheSameStrippedSubject = new QList< MessageItem * >(); mThreadingCacheMessageSubjectMD5ToMessageItem.insert(mi->strippedSubjectMD5(), messagesWithTheSameStrippedSubject); messagesWithTheSameStrippedSubject->append(mi); return; } // Found: assert that we have no duplicates in the cache. Q_ASSERT(!messagesWithTheSameStrippedSubject->contains(mi)); // Ordered insert: first by date then by pointer value. auto it = std::lower_bound(messagesWithTheSameStrippedSubject->begin(), messagesWithTheSameStrippedSubject->end(), mi, MessageLessThanByDate()); messagesWithTheSameStrippedSubject->insert(it, mi); } void ModelPrivate::removeMessageFromSubjectBasedThreadingCache(MessageItem *mi) { // We assume that the caller knows what he is doing and the message is actually in the cache. // If the message isn't in the cache then we should not be called at all. // // The game is called "performance" // Grab the list of all the messages with the same stripped subject (all potential parents) auto messagesWithTheSameStrippedSubject = mThreadingCacheMessageSubjectMD5ToMessageItem.value(mi->strippedSubjectMD5(), nullptr); // We assume that the message is there so the list must be non null. Q_ASSERT(messagesWithTheSameStrippedSubject); // The cache *MUST* be ordered first by date then by pointer value auto it = std::lower_bound(messagesWithTheSameStrippedSubject->begin(), messagesWithTheSameStrippedSubject->end(), mi, MessageLessThanByDate()); // The binary based search must have found a message Q_ASSERT(it != messagesWithTheSameStrippedSubject->end()); // and it must have found exactly the message requested Q_ASSERT(*it == mi); // Kill it messagesWithTheSameStrippedSubject->erase(it); // And kill the list if it was the last one if (messagesWithTheSameStrippedSubject->isEmpty()) { mThreadingCacheMessageSubjectMD5ToMessageItem.remove(mi->strippedSubjectMD5()); delete messagesWithTheSameStrippedSubject; } } MessageItem *ModelPrivate::guessMessageParent(MessageItem *mi) { // This function implements subject based threading // It attempts to guess a thread parent for the item "mi" // which actually may already have a children subtree. // We have all the problems of findMessageParent() plus the fact that // we're actually guessing (and often we may be *wrong*). Q_ASSERT(mAggregation->threading() == Aggregation::PerfectReferencesAndSubject); // caller must take care of this Q_ASSERT(mi->subjectIsPrefixed()); // caller must take care of this Q_ASSERT(mi->threadingStatus() == MessageItem::ParentMissing); // Do subject based threading const QByteArray md5 = mi->strippedSubjectMD5(); if (!md5.isEmpty()) { auto messagesWithTheSameStrippedSubject = mThreadingCacheMessageSubjectMD5ToMessageItem.value(md5, nullptr); if (messagesWithTheSameStrippedSubject) { Q_ASSERT(!messagesWithTheSameStrippedSubject->isEmpty()); // Need to find the message with the maximum date lower than the one of this message time_t maxTime = (time_t)0; MessageItem *pParent = nullptr; // Here'we re really guessing so circular references are possible // even on perfectly valid trees. This is why we don't consider it // an error but just continue searching. // FIXME: This might be speed up with an initial binary search (?) // ANSWER: No. We can't rely on date order (as it can be updated on the fly...) for (const auto it : qAsConst(*messagesWithTheSameStrippedSubject)) { int delta = mi->date() - it->date(); // We don't take into account messages with a delta smaller than 120. // Assuming that our date() values are correct (that is, they take into // account timezones etc..) then one usually needs more than 120 seconds // to answer to a message. Better safe than sorry. // This check also includes negative deltas so messages later than mi aren't considered if (delta < 120) { break; // The list is ordered by date (ascending) so we can stop searching here } // About the "magic" 3628899 value here comes a Till's comment from the original KMHeaders: // // "Parents more than six weeks older than the message are not accepted. The reasoning being // that if a new message with the same subject turns up after such a long time, the chances // that it is still part of the same thread are slim. The value of six weeks is chosen as a // result of a poll conducted on kde-devel, so it's probably bogus. :)" if (delta < 3628899) { // Compute the closest. if ((maxTime < it->date())) { // This algorithm *can* be (and often is) wrong. // Take care of circular threading which is really possible at this level. // If mi contains "it" inside its children subtree then we have // found such a circular threading problem. // Note that here we can't have it == mi because of the delta >= 120 check above. if ((mi->childItemCount() == 0) || !it->hasAncestor(mi)) { maxTime = it->date(); pParent = it; } } } } if (pParent) { mi->setThreadingStatus(MessageItem::ImperfectParentFound); return pParent; // got an imperfect parent for this message } } } return nullptr; } // // A little template helper, hopefully inlineable. // // Return true if the specified message item is in the wrong position // inside the specified parent and needs re-sorting. Return false otherwise. // Both parent and messageItem must not be null. // // Checking if a message needs re-sorting instead of just re-sorting it // is very useful since re-sorting is an expensive operation. // template< class ItemComparator > static bool messageItemNeedsReSorting(SortOrder::SortDirection messageSortDirection, ItemPrivate *parent, MessageItem *messageItem) { if ((messageSortDirection == SortOrder::Ascending) || (parent->mType == Item::Message)) { return parent->childItemNeedsReSorting< ItemComparator, true >(messageItem); } return parent->childItemNeedsReSorting< ItemComparator, false >(messageItem); } bool ModelPrivate::handleItemPropertyChanges(int propertyChangeMask, Item *parent, Item *item) { // The facts: // // - If dates changed: // - If we're sorting messages by min/max date then at each level the messages might need resorting. // - If the thread leader is the most recent message of a thread then the uppermost // message of the thread might need re-grouping. // - If the groups are sorted by min/max date then the group might need re-sorting too. // // This function explicitly doesn't re-apply the filter when ActionItemStatus changes. // This is because filters must be re-applied due to a broader range of status variations: // this is done in viewItemJobStepInternalForJobPass1Update() instead (which is the only // place in that ActionItemStatus may be set). if (parent->type() == Item::InvisibleRoot) { // item is either a message or a group attached to the root. // It might need resorting. if (item->type() == Item::GroupHeader) { // item is a group header attached to the root. if ( ( // max date changed (propertyChangeMask & MaxDateChanged) &&// groups sorted by max date (mSortOrder->groupSorting() == SortOrder::SortGroupsByDateTimeOfMostRecent) ) || ( // date changed (propertyChangeMask & DateChanged) &&// groups sorted by date (mSortOrder->groupSorting() == SortOrder::SortGroupsByDateTime) ) ) { // This group might need re-sorting. // Groups are large container of messages so it's likely that // another message inserted will cause this group to be marked again. // So we wait until the end to do the grand final re-sorting: it will be done in Pass4. mGroupHeadersThatNeedUpdate.insert(static_cast< GroupHeaderItem * >(item), static_cast< GroupHeaderItem * >(item)); } } else { // item is a message. It might need re-sorting. // Since sorting is an expensive operation, we first check if it's *really* needed. // Re-sorting will actually not change min/max dates at all and // will not climb up the parent's ancestor tree. switch (mSortOrder->messageSorting()) { case SortOrder::SortMessagesByDateTime: if (propertyChangeMask & DateChanged) { // date changed if (messageItemNeedsReSorting< ItemDateComparator >(mSortOrder->messageSortDirection(), parent->d_ptr, static_cast< MessageItem * >(item))) { attachMessageToParent(parent, static_cast< MessageItem * >(item)); } } // else date changed, but it doesn't match sorting order: no need to re-sort break; case SortOrder::SortMessagesByDateTimeOfMostRecent: if (propertyChangeMask & MaxDateChanged) { // max date changed if (messageItemNeedsReSorting< ItemMaxDateComparator >(mSortOrder->messageSortDirection(), parent->d_ptr, static_cast< MessageItem * >(item))) { attachMessageToParent(parent, static_cast< MessageItem * >(item)); } } // else max date changed, but it doesn't match sorting order: no need to re-sort break; case SortOrder::SortMessagesByActionItemStatus: if (propertyChangeMask & ActionItemStatusChanged) { // todo status changed if (messageItemNeedsReSorting< ItemActionItemStatusComparator >(mSortOrder->messageSortDirection(), parent->d_ptr, static_cast< MessageItem * >(item))) { attachMessageToParent(parent, static_cast< MessageItem * >(item)); } } // else to do status changed, but it doesn't match sorting order: no need to re-sort break; case SortOrder::SortMessagesByUnreadStatus: if (propertyChangeMask & UnreadStatusChanged) { // new / unread status changed if (messageItemNeedsReSorting< ItemUnreadStatusComparator >(mSortOrder->messageSortDirection(), parent->d_ptr, static_cast< MessageItem * >(item))) { attachMessageToParent(parent, static_cast< MessageItem * >(item)); } } // else new/unread status changed, but it doesn't match sorting order: no need to re-sort break; case SortOrder::SortMessagesByImportantStatus: if (propertyChangeMask & ImportantStatusChanged) { // important status changed if (messageItemNeedsReSorting< ItemImportantStatusComparator >(mSortOrder->messageSortDirection(), parent->d_ptr, static_cast< MessageItem * >(item))) { attachMessageToParent(parent, static_cast< MessageItem * >(item)); } } // else new/unread status changed, but it doesn't match sorting order: no need to re-sort break; case SortOrder::SortMessagesByAttachmentStatus: if (propertyChangeMask & AttachmentStatusChanged) { // attachment status changed if (messageItemNeedsReSorting< ItemAttachmentStatusComparator >(mSortOrder->messageSortDirection(), parent->d_ptr, static_cast< MessageItem * >(item))) { attachMessageToParent(parent, static_cast< MessageItem * >(item)); } } // else new/unread status changed, but it doesn't match sorting order: no need to re-sort break; default: // this kind of message sorting isn't affected by the property changes: nothing to do. break; } } return false; // the invisible root isn't affected by any change. } if (parent->type() == Item::GroupHeader) { // item is a message attached to a GroupHeader. // It might need re-grouping or re-sorting (within the same group) // Check re-grouping here. if ( ( // max date changed (propertyChangeMask & MaxDateChanged) &&// thread leader is most recent message (mAggregation->threadLeader() == Aggregation::MostRecentMessage) ) || ( // date changed (propertyChangeMask & DateChanged) &&// thread leader the topmost message (mAggregation->threadLeader() == Aggregation::TopmostMessage) ) ) { // Might really need re-grouping. // attachMessageToGroupHeader() will find the right group for this message // and if it's different than the current it will move it. attachMessageToGroupHeader(static_cast< MessageItem * >(item)); // Re-grouping fixes the properties of the involved group headers // so at exit of attachMessageToGroupHeader() the parent can't be affected // by the change anymore. return false; } // Re-grouping wasn't needed. Re-sorting might be. } // else item is a message attached to another message and might need re-sorting only. // Check if message needs re-sorting. switch (mSortOrder->messageSorting()) { case SortOrder::SortMessagesByDateTime: if (propertyChangeMask & DateChanged) { // date changed if (messageItemNeedsReSorting< ItemDateComparator >(mSortOrder->messageSortDirection(), parent->d_ptr, static_cast< MessageItem * >(item))) { attachMessageToParent(parent, static_cast< MessageItem * >(item)); } } // else date changed, but it doesn't match sorting order: no need to re-sort break; case SortOrder::SortMessagesByDateTimeOfMostRecent: if (propertyChangeMask & MaxDateChanged) { // max date changed if (messageItemNeedsReSorting< ItemMaxDateComparator >(mSortOrder->messageSortDirection(), parent->d_ptr, static_cast< MessageItem * >(item))) { attachMessageToParent(parent, static_cast< MessageItem * >(item)); } } // else max date changed, but it doesn't match sorting order: no need to re-sort break; case SortOrder::SortMessagesByActionItemStatus: if (propertyChangeMask & ActionItemStatusChanged) { // todo status changed if (messageItemNeedsReSorting< ItemActionItemStatusComparator >(mSortOrder->messageSortDirection(), parent->d_ptr, static_cast< MessageItem * >(item))) { attachMessageToParent(parent, static_cast< MessageItem * >(item)); } } // else to do status changed, but it doesn't match sorting order: no need to re-sort break; case SortOrder::SortMessagesByUnreadStatus: if (propertyChangeMask & UnreadStatusChanged) { // new / unread status changed if (messageItemNeedsReSorting< ItemUnreadStatusComparator >(mSortOrder->messageSortDirection(), parent->d_ptr, static_cast< MessageItem * >(item))) { attachMessageToParent(parent, static_cast< MessageItem * >(item)); } } // else new/unread status changed, but it doesn't match sorting order: no need to re-sort break; case SortOrder::SortMessagesByImportantStatus: if (propertyChangeMask & ImportantStatusChanged) { // important status changed if (messageItemNeedsReSorting< ItemImportantStatusComparator >(mSortOrder->messageSortDirection(), parent->d_ptr, static_cast< MessageItem * >(item))) { attachMessageToParent(parent, static_cast< MessageItem * >(item)); } } // else important status changed, but it doesn't match sorting order: no need to re-sort break; case SortOrder::SortMessagesByAttachmentStatus: if (propertyChangeMask & AttachmentStatusChanged) { // attachment status changed if (messageItemNeedsReSorting< ItemAttachmentStatusComparator >(mSortOrder->messageSortDirection(), parent->d_ptr, static_cast< MessageItem * >(item))) { attachMessageToParent(parent, static_cast< MessageItem * >(item)); } } // else important status changed, but it doesn't match sorting order: no need to re-sort break; default: // this kind of message sorting isn't affected by property changes: nothing to do. break; } return true; // parent might be affected too. } void ModelPrivate::messageDetachedUpdateParentProperties(Item *oldParent, MessageItem *mi) { Q_ASSERT(oldParent); Q_ASSERT(mi); Q_ASSERT(oldParent != mRootItem); // oldParent might have its properties changed because of the child removal. // propagate the changes up. for (;;) { // pParent is not the root item now. This is assured by how we enter this loop // and by the fact that handleItemPropertyChanges returns false when grandParent // is Item::InvisibleRoot. We could actually assert it here... // Check if its dates need an update. int propertyChangeMask; if ((mi->maxDate() == oldParent->maxDate()) && oldParent->recomputeMaxDate()) { propertyChangeMask = MaxDateChanged; } else { break; // from the for(;;) loop } // One of the oldParent properties has changed for sure Item *grandParent = oldParent->parent(); // If there is no grandParent then oldParent isn't attached to the view. // Re-sorting / re-grouping isn't needed for sure. if (!grandParent) { break; // from the for(;;) loop } // The following function will return true if grandParent may be affected by the change. // If the grandParent isn't affected, we stop climbing. if (!handleItemPropertyChanges(propertyChangeMask, grandParent, oldParent)) { break; // from the for(;;) loop } // Now we need to climb up one level and check again. oldParent = grandParent; } // for(;;) loop // If the last message was removed from a group header then this group will need an update // for sure. We will need to remove it (unless a message is attached back to it) if (oldParent->type() == Item::GroupHeader) { if (oldParent->childItemCount() == 0) { mGroupHeadersThatNeedUpdate.insert(static_cast< GroupHeaderItem * >(oldParent), static_cast< GroupHeaderItem * >(oldParent)); } } } void ModelPrivate::propagateItemPropertiesToParent(Item *item) { Item *pParent = item->parent(); Q_ASSERT(pParent); Q_ASSERT(pParent != mRootItem); for (;;) { // pParent is not the root item now. This is assured by how we enter this loop // and by the fact that handleItemPropertyChanges returns false when grandParent // is Item::InvisibleRoot. We could actually assert it here... // Check if its dates need an update. int propertyChangeMask; if (item->maxDate() > pParent->maxDate()) { pParent->setMaxDate(item->maxDate()); propertyChangeMask = MaxDateChanged; } else { // No parent dates have changed: no further work is needed. Stop climbing here. break; // from the for(;;) loop } // One of the pParent properties has changed. Item *grandParent = pParent->parent(); // If there is no grandParent then pParent isn't attached to the view. // Re-sorting / re-grouping isn't needed for sure. if (!grandParent) { break; // from the for(;;) loop } // The following function will return true if grandParent may be affected by the change. // If the grandParent isn't affected, we stop climbing. if (!handleItemPropertyChanges(propertyChangeMask, grandParent, pParent)) { break; // from the for(;;) loop } // Now we need to climb up one level and check again. pParent = grandParent; } // for(;;) } void ModelPrivate::attachMessageToParent(Item *pParent, MessageItem *mi, AttachOptions attachOptions) { Q_ASSERT(pParent); Q_ASSERT(mi); // This function may be called to do a simple "re-sort" of the item inside the parent. // In that case mi->parent() is equal to pParent. bool oldParentWasTheSame; if (mi->parent()) { Item *oldParent = mi->parent(); // The item already had a parent and this means that we're moving it. oldParentWasTheSame = oldParent == pParent; // just re-sorting ? if (mi->isViewable()) { // is actually // The message is actually attached to the viewable root // Unfortunately we need to hack the model/view architecture // since it's somewhat flawed in this. At the moment of writing // there is simply no way to atomically move a subtree. // We must detach, call beginRemoveRows()/endRemoveRows(), // save the expanded state, save the selection, save the current item, // save the view position (YES! As we are removing items the view // will hopelessly jump around so we're just FORCED to break // the isolation from the view)... // ...*then* reattach, restore the expanded state, restore the selection, // restore the current item, restore the view position and pray // that nothing will fail in the (rather complicated) process.... // Yet more unfortunately, while saving the expanded state might stop // at a certain (unexpanded) point in the tree, saving the selection // is hopelessly recursive down to the bare leafs. // Furthermore the expansion of items is a common case while selection // in the subtree is rare, so saving it would be a huge cost with // a low revenue. // This is why we just let the selection screw up. I hereby refuse to call // yet another expensive recursive function here :D // The current item saving can be somewhat optimized doing it once for // a single job step... if ( ((mi)->childItemCount() > 0) // has children && mModelForItemFunctions // the UI is not actually disconnected && mView->isExpanded(q->index(mi, 0)) // is actually expanded ) { saveExpandedStateOfSubtree(mi); } } // If the parent is viewable (so mi was viewable too) then the beginRemoveRows() // and endRemoveRows() functions of this model will be called too. oldParent->takeChildItem(mModelForItemFunctions, mi); if ((!oldParentWasTheSame) && (oldParent != mRootItem)) { messageDetachedUpdateParentProperties(oldParent, mi); } } else { // The item had no parent yet. oldParentWasTheSame = false; } // Take care of perfect / imperfect threading. // Items that are now perfectly threaded, but already have a different parent // might have been imperfectly threaded before. Remove them from the caches. // Items that are now imperfectly threaded must be added to the caches. // // If we're just re-sorting the item inside the same parent then the threading // caches don't need to be updated (since they actually depend on the parent). if (!oldParentWasTheSame) { switch (mi->threadingStatus()) { case MessageItem::PerfectParentFound: if (!mi->inReplyToIdMD5().isEmpty()) { mThreadingCacheMessageInReplyToIdMD5ToMessageItem.remove(mi->inReplyToIdMD5(), mi); } if (attachOptions == StoreInCache && pParent->type() == Item::Message) { mThreadingCache.updateParent(mi, static_cast(pParent)); } break; case MessageItem::ImperfectParentFound: case MessageItem::ParentMissing: // may be: temporary or just fallback assignment if (!mi->inReplyToIdMD5().isEmpty()) { if (!mThreadingCacheMessageInReplyToIdMD5ToMessageItem.contains(mi->inReplyToIdMD5(), mi)) { mThreadingCacheMessageInReplyToIdMD5ToMessageItem.insert(mi->inReplyToIdMD5(), mi); } } break; case MessageItem::NonThreadable: // this also happens when we do no threading at all // make gcc happy Q_ASSERT(!mThreadingCacheMessageInReplyToIdMD5ToMessageItem.contains(mi->inReplyToIdMD5(), mi)); break; } } // Set the new parent mi->setParent(pParent); // Propagate watched and ignored status if ( (pParent->status().toQInt32() & mCachedWatchedOrIgnoredStatusBits) // unlikely && (pParent->type() == Item::Message) // likely ) { // the parent is either watched or ignored: propagate to the child if (pParent->status().isWatched()) { int row = mInvariantRowMapper->modelInvariantIndexToModelIndexRow(mi); mi->setStatus(Akonadi::MessageStatus::statusWatched()); mStorageModel->setMessageItemStatus(mi, row, Akonadi::MessageStatus::statusWatched()); } else if (pParent->status().isIgnored()) { int row = mInvariantRowMapper->modelInvariantIndexToModelIndexRow(mi); mi->setStatus(Akonadi::MessageStatus::statusIgnored()); mStorageModel->setMessageItemStatus(mi, row, Akonadi::MessageStatus::statusIgnored()); } } // And insert into its child list // If pParent is viewable then the insert/append functions will call this model's // beginInsertRows() and endInsertRows() functions. This is EXTREMELY // expensive and ugly but it's the only way with the Qt4 imposed Model/View method. // Dude... (citation from Lost, if it wasn't clear). // I'm using a macro since it does really improve readability. // I'm NOT using a helper function since gcc will refuse to inline some of // the calls because they make this function grow too much. #define INSERT_MESSAGE_WITH_COMPARATOR(_ItemComparator) \ if ((mSortOrder->messageSortDirection() == SortOrder::Ascending) \ || (pParent->type() == Item::Message)) \ { \ pParent->d_ptr->insertChildItem< _ItemComparator, true >(mModelForItemFunctions, mi); \ } \ else \ { \ pParent->d_ptr->insertChildItem< _ItemComparator, false >(mModelForItemFunctions, mi); \ } // If pParent is viewable then the insertion call will also set the child state to viewable. // Since mi MAY have children, then this call may make them viewable. switch (mSortOrder->messageSorting()) { case SortOrder::SortMessagesByDateTime: INSERT_MESSAGE_WITH_COMPARATOR(ItemDateComparator) break; case SortOrder::SortMessagesByDateTimeOfMostRecent: INSERT_MESSAGE_WITH_COMPARATOR(ItemMaxDateComparator) break; case SortOrder::SortMessagesBySize: INSERT_MESSAGE_WITH_COMPARATOR(ItemSizeComparator) break; case SortOrder::SortMessagesBySenderOrReceiver: INSERT_MESSAGE_WITH_COMPARATOR(ItemSenderOrReceiverComparator) break; case SortOrder::SortMessagesBySender: INSERT_MESSAGE_WITH_COMPARATOR(ItemSenderComparator) break; case SortOrder::SortMessagesByReceiver: INSERT_MESSAGE_WITH_COMPARATOR(ItemReceiverComparator) break; case SortOrder::SortMessagesBySubject: INSERT_MESSAGE_WITH_COMPARATOR(ItemSubjectComparator) break; case SortOrder::SortMessagesByActionItemStatus: INSERT_MESSAGE_WITH_COMPARATOR(ItemActionItemStatusComparator) break; case SortOrder::SortMessagesByUnreadStatus: INSERT_MESSAGE_WITH_COMPARATOR(ItemUnreadStatusComparator) break; case SortOrder::SortMessagesByImportantStatus: INSERT_MESSAGE_WITH_COMPARATOR(ItemImportantStatusComparator) break; case SortOrder::SortMessagesByAttachmentStatus: INSERT_MESSAGE_WITH_COMPARATOR(ItemAttachmentStatusComparator) break; case SortOrder::NoMessageSorting: pParent->appendChildItem(mModelForItemFunctions, mi); break; default: // should never happen pParent->appendChildItem(mModelForItemFunctions, mi); break; } // Decide if we need to expand parents bool childNeedsExpanding = (mi->initialExpandStatus() == Item::ExpandNeeded); if (pParent->initialExpandStatus() == Item::NoExpandNeeded) { switch (mAggregation->threadExpandPolicy()) { case Aggregation::NeverExpandThreads: // just do nothing unless this child has children and is already marked for expansion if (childNeedsExpanding) { pParent->setInitialExpandStatus(Item::ExpandNeeded); } break; case Aggregation::ExpandThreadsWithNewMessages: // No more new status. fall through to unread if it exists in config case Aggregation::ExpandThreadsWithUnreadMessages: // expand only if unread (or it has children marked for expansion) if (childNeedsExpanding || !mi->status().isRead()) { pParent->setInitialExpandStatus(Item::ExpandNeeded); } break; case Aggregation::ExpandThreadsWithUnreadOrImportantMessages: // expand only if unread, important or todo (or it has children marked for expansion) // FIXME: Wouldn't it be nice to be able to test for bitmasks in MessageStatus ? if (childNeedsExpanding || !mi->status().isRead() || mi->status().isImportant() || mi->status().isToAct()) { pParent->setInitialExpandStatus(Item::ExpandNeeded); } break; case Aggregation::AlwaysExpandThreads: // expand everything pParent->setInitialExpandStatus(Item::ExpandNeeded); break; default: // BUG break; } } // else it's already marked for expansion or expansion has been already executed // expand parent first, if possible if (pParent->initialExpandStatus() == Item::ExpandNeeded) { // If UI is not disconnected and parent is viewable, go up and expand if (mModelForItemFunctions && pParent->isViewable()) { // Now expand parents as needed Item *parentToExpand = pParent; while (parentToExpand) { if (parentToExpand == mRootItem) { break; // no need to set it expanded } // parentToExpand is surely viewable (because this item is) if (parentToExpand->initialExpandStatus() == Item::ExpandExecuted) { break; } mView->expand(q->index(parentToExpand, 0)); parentToExpand->setInitialExpandStatus(Item::ExpandExecuted); parentToExpand = parentToExpand->parent(); } } else { // It isn't viewable or UI is disconnected: climb up marking only Item *parentToExpand = pParent->parent(); while (parentToExpand) { if (parentToExpand == mRootItem) { break; // no need to set it expanded } parentToExpand->setInitialExpandStatus(Item::ExpandNeeded); parentToExpand = parentToExpand->parent(); } } } if (mi->isViewable()) { // mi is now viewable // sync subtree expanded status if (childNeedsExpanding) { if (mi->childItemCount() > 0) { if (mModelForItemFunctions) { // the UI is not disconnected syncExpandedStateOfSubtree(mi); // sync the real state in the view } } } // apply the filter, if needed if (mFilter) { Q_ASSERT(mModelForItemFunctions); // the UI must be NOT disconnected here // apply the filter to subtree if (applyFilterToSubtree(mi, q->index(pParent, 0))) { // mi matched, expand parents (unconditionally) mView->ensureDisplayedWithParentsExpanded(mi); } } } // Now we need to propagate the property changes the upper levels. // If we have just inserted a message inside the root then no work needs to be done: // no grouping is in effect and the message is already in the right place. if (pParent == mRootItem) { return; } // If we have just removed the item from this parent and re-inserted it // then this operation was a simple re-sort. The code above didn't update // the properties when removing the item so we don't actually need // to make the updates back. if (oldParentWasTheSame) { return; } // FIXME: OPTIMIZE THIS: First propagate changes THEN syncExpandedStateOfSubtree() // and applyFilterToSubtree... (needs some thinking though). // Time to propagate up. propagateItemPropertiesToParent(mi); // Aaah.. we're done. Time for a thea ? :) } // FIXME: ThreadItem ? // // Foo Bar, Joe Thommason, Martin Rox ... Eddie Maiden // Title , Last by xxx // // When messages are added, mark it as dirty only (?) ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJobPass5(ViewItemJob *job, QElapsedTimer elapsedTimer) { // In this pass we scan the group headers that are in mGroupHeadersThatNeedUpdate. // Empty groups get deleted while the other ones are re-sorted. int curIndex = job->currentIndex(); auto it = mGroupHeadersThatNeedUpdate.begin(); auto end = mGroupHeadersThatNeedUpdate.end(); while (it != end) { if ((*it)->childItemCount() == 0) { // group with no children, kill it (*it)->parent()->takeChildItem(mModelForItemFunctions, *it); mGroupHeaderItemHash.remove((*it)->label()); // If we were going to restore its position after the job step, well.. we can't do it anymore. if (mCurrentItemToRestoreAfterViewItemJobStep == (*it)) { mCurrentItemToRestoreAfterViewItemJobStep = nullptr; } // bye bye delete *it; } else { // Group with children: probably needs re-sorting. // Re-sorting here is an expensive operation. // In fact groups have been put in the QHash above on the assumption // that re-sorting *might* be needed but no real (expensive) check // has been done yet. Also by sorting a single group we might actually // put the others in the right place. // So finally check if re-sorting is *really* needed. bool needsReSorting; // A macro really improves readability here. #define CHECK_IF_GROUP_NEEDS_RESORTING(_ItemDateComparator) \ switch (mSortOrder->groupSortDirection()) \ { \ case SortOrder::Ascending: \ needsReSorting = (*it)->parent()->d_ptr->childItemNeedsReSorting< _ItemDateComparator, true >(*it); \ break; \ case SortOrder::Descending: \ needsReSorting = (*it)->parent()->d_ptr->childItemNeedsReSorting< _ItemDateComparator, false >(*it); \ break; \ default: /* should never happen */ \ needsReSorting = false; \ break; \ } switch (mSortOrder->groupSorting()) { case SortOrder::SortGroupsByDateTime: CHECK_IF_GROUP_NEEDS_RESORTING(ItemDateComparator) break; case SortOrder::SortGroupsByDateTimeOfMostRecent: CHECK_IF_GROUP_NEEDS_RESORTING(ItemMaxDateComparator) break; case SortOrder::SortGroupsBySenderOrReceiver: CHECK_IF_GROUP_NEEDS_RESORTING(ItemSenderOrReceiverComparator) break; case SortOrder::SortGroupsBySender: CHECK_IF_GROUP_NEEDS_RESORTING(ItemSenderComparator) break; case SortOrder::SortGroupsByReceiver: CHECK_IF_GROUP_NEEDS_RESORTING(ItemReceiverComparator) break; case SortOrder::NoGroupSorting: needsReSorting = false; break; default: // Should never happen... just assume re-sorting is not needed needsReSorting = false; break; } if (needsReSorting) { attachGroup(*it); // it will first detach and then re-attach in the proper place } } it = mGroupHeadersThatNeedUpdate.erase(it); curIndex++; // FIXME: In fact a single update is likely to manipulate // a subtree with a LOT of messages inside. If interactivity is favored // we should check the time really more often. if ((curIndex % mViewItemJobStepMessageCheckCount) == 0) { if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) { if (it != mGroupHeadersThatNeedUpdate.end()) { job->setCurrentIndex(curIndex); return ViewItemJobInterrupted; } } } } return ViewItemJobCompleted; } ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJobPass4(ViewItemJob *job, QElapsedTimer elapsedTimer) { // In this pass we scan mUnassignedMessageListForPass4 which now // contains both items with parents and items without parents. // We scan mUnassignedMessageList for messages without parent (the ones that haven't been // attached to the viewable tree yet) and find a suitable group for them. Then we simply // clear mUnassignedMessageList. // We call this pass "Grouping" int curIndex = job->currentIndex(); int endIndex = job->endIndex(); while (curIndex <= endIndex) { MessageItem *mi = mUnassignedMessageListForPass4[curIndex]; if (!mi->parent()) { // Unassigned item: thread leader, insert into the proper group. // Locate the group (or root if no grouping requested) attachMessageToGroupHeader(mi); } else { // A parent was already assigned in Pass3: we have nothing to do here } curIndex++; // FIXME: In fact a single call to attachMessageToGroupHeader() is likely to manipulate // a subtree with a LOT of messages inside. If interactivity is favored // we should check the time really more often. if ((curIndex % mViewItemJobStepMessageCheckCount) == 0) { if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) { if (curIndex <= endIndex) { job->setCurrentIndex(curIndex); return ViewItemJobInterrupted; } } } } mUnassignedMessageListForPass4.clear(); return ViewItemJobCompleted; } ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJobPass3(ViewItemJob *job, QElapsedTimer elapsedTimer) { // In this pass we scan the mUnassignedMessageListForPass3 and try to do construct the threads // by using subject based threading. If subject based threading is not in effect then // this pass turns to a nearly-no-op: at the end of Pass2 we have swapped the lists // and mUnassignedMessageListForPass3 is actually empty. // We don't shrink the mUnassignedMessageListForPass3 for two reasons: // - It would mess up this chunked algorithm by shifting indexes // - mUnassignedMessageList is a QList which is basically an array. It's faster // to traverse an array of N entries than to remove K>0 entries one by one and // to traverse the remaining N-K entries. int curIndex = job->currentIndex(); int endIndex = job->endIndex(); while (curIndex <= endIndex) { // If we're here, then threading is requested for sure. auto mi = mUnassignedMessageListForPass3[curIndex]; if ((!mi->parent()) || (mi->threadingStatus() == MessageItem::ParentMissing)) { // Parent is missing (either "physically" with the item being not attached or "logically" // with the item being attached to a group or directly to the root. if (mi->subjectIsPrefixed()) { // We can try to guess it auto mparent = guessMessageParent(mi); if (mparent) { // imperfect parent found if (mi->isViewable()) { // mi was already viewable, we're just trying to re-parent it better... attachMessageToParent(mparent, mi); if (!mparent->isViewable()) { // re-attach it immediately (so current item is not lost) auto topmost = mparent->topmostMessage(); Q_ASSERT(!topmost->parent()); // groups are always viewable! topmost->setThreadingStatus(MessageItem::ParentMissing); attachMessageToGroupHeader(topmost); } } else { // mi wasn't viewable yet.. no need to attach parent attachMessageToParent(mparent, mi); } // and we're done for now } else { // so parent not found, (threadingStatus() is either MessageItem::ParentMissing or MessageItem::NonThreadable) Q_ASSERT((mi->threadingStatus() == MessageItem::ParentMissing) || (mi->threadingStatus() == MessageItem::NonThreadable)); mUnassignedMessageListForPass4.append(mi); // this is ~O(1) // and wait for Pass4 } } else { // can't guess the parent as the subject isn't prefixed Q_ASSERT((mi->threadingStatus() == MessageItem::ParentMissing) || (mi->threadingStatus() == MessageItem::NonThreadable)); mUnassignedMessageListForPass4.append(mi); // this is ~O(1) // and wait for Pass4 } } else { // Has a parent: either perfect parent already found or non threadable. // Since we don't end here if mi has status of parent missing then mi must not have imperfect parent. Q_ASSERT(mi->threadingStatus() != MessageItem::ImperfectParentFound); Q_ASSERT(mi->isViewable()); } curIndex++; // FIXME: In fact a single call to attachMessageToGroupHeader() is likely to manipulate // a subtree with a LOT of messages inside. If interactivity is favored // we should check the time really more often. if ((curIndex % mViewItemJobStepMessageCheckCount) == 0) { if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) { if (curIndex <= endIndex) { job->setCurrentIndex(curIndex); return ViewItemJobInterrupted; } } } } mUnassignedMessageListForPass3.clear(); return ViewItemJobCompleted; } ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJobPass2(ViewItemJob *job, QElapsedTimer elapsedTimer) { // In this pass we scan the mUnassignedMessageList and try to do construct the threads. // If some thread leader message got attached to the viewable tree in Pass1Fill then // we'll also attach all of its children too. The thread leaders we were unable // to attach in Pass1Fill and their children (which we find here) will make it to the small Pass3 // We don't shrink the mUnassignedMessageList for two reasons: // - It would mess up this chunked algorithm by shifting indexes // - mUnassignedMessageList is a QList which is basically an array. It's faster // to traverse an array of N entries than to remove K>0 entries one by one and // to traverse the remaining N-K entries. // We call this pass "Threading" int curIndex = job->currentIndex(); int endIndex = job->endIndex(); while (curIndex <= endIndex) { // If we're here, then threading is requested for sure. auto mi = mUnassignedMessageListForPass2[curIndex]; // The item may or may not have a parent. // If it has no parent or it has a temporary one (mi->parent() && mi->threadingStatus() == MessageItem::ParentMissing) // then we attempt to (re-)thread it. Otherwise we just do nothing (the job has already been done by the previous steps). if ((!mi->parent()) || (mi->threadingStatus() == MessageItem::ParentMissing)) { qint64 parentId; auto mparent = mThreadingCache.parentForItem(mi, parentId); if (mparent && !mparent->hasAncestor(mi)) { mi->setThreadingStatus(MessageItem::PerfectParentFound); attachMessageToParent(mparent, mi, SkipCacheUpdate); } else { if (parentId > 0) { // In second pass we have all available Items in mThreadingCache already. If // mThreadingCache.parentForItem() returns null, but returns valid parentId then // the Item was removed from Akonadi and our threading cache is out-of-date. mThreadingCache.expireParent(mi); mparent = findMessageParent(mi); } else if (parentId < 0) { mparent = findMessageParent(mi); } else { // parentId = 0: this message is a thread leader so don't // bother resolving parent, it will be moved directly to // Pass4 in the code below } if (mparent) { // parent found, either perfect or imperfect if (mi->isViewable()) { // mi was already viewable, we're just trying to re-parent it better... attachMessageToParent(mparent, mi); if (!mparent->isViewable()) { // re-attach it immediately (so current item is not lost) auto topmost = mparent->topmostMessage(); Q_ASSERT(!topmost->parent()); // groups are always viewable! topmost->setThreadingStatus(MessageItem::ParentMissing); attachMessageToGroupHeader(topmost); } } else { // mi wasn't viewable yet.. no need to attach parent attachMessageToParent(mparent, mi); } // and we're done for now } else { // so parent not found, (threadingStatus() is either MessageItem::ParentMissing or MessageItem::NonThreadable) switch (mi->threadingStatus()) { case MessageItem::ParentMissing: if (mAggregation->threading() == Aggregation::PerfectReferencesAndSubject) { // parent missing but still can be found in Pass3 mUnassignedMessageListForPass3.append(mi); // this is ~O(1) } else { // We're not doing subject based threading: will never be threaded, go straight to Pass4 mUnassignedMessageListForPass4.append(mi); // this is ~O(1) } break; case MessageItem::NonThreadable: // will never be threaded, go straight to Pass4 mUnassignedMessageListForPass4.append(mi); // this is ~O(1) break; default: // a bug for sure qCWarning(MESSAGELIST_LOG) << "ERROR: Invalid message threading status returned by findMessageParent()!"; Q_ASSERT(false); break; } } } } else { // Has a parent: either perfect parent already found or non threadable. // Since we don't end here if mi has status of parent missing then mi must not have imperfect parent. Q_ASSERT(mi->threadingStatus() != MessageItem::ImperfectParentFound); if (!mi->isViewable()) { qCWarning(MESSAGELIST_LOG) << "Non viewable message " << mi << " subject " << mi->subject().toUtf8().data(); Q_ASSERT(mi->isViewable()); } } curIndex++; // FIXME: In fact a single call to attachMessageToGroupHeader() is likely to manipulate // a subtree with a LOT of messages inside. If interactivity is favored // we should check the time really more often. if ((curIndex % mViewItemJobStepMessageCheckCount) == 0) { if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) { if (curIndex <= endIndex) { job->setCurrentIndex(curIndex); return ViewItemJobInterrupted; } } } } mUnassignedMessageListForPass2.clear(); return ViewItemJobCompleted; } ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJobPass1Fill(ViewItemJob *job, QElapsedTimer elapsedTimer) { // In this pass we scan the a contiguous region of the underlying storage (that is // assumed to be FLAT) and create the corresponding MessageItem objects. // The deal is to show items to the user as soon as possible so in this pass we // *TRY* to attach them to the viewable tree (which is rooted on mRootItem). // Messages we're unable to attach for some reason (mainly due to threading) get appended // to mUnassignedMessageList and wait for Pass2. // We call this pass "Processing" // Should we use the receiver or the sender field for sorting ? bool bUseReceiver = mStorageModelContainsOutboundMessages; // The begin storage index of our work int curIndex = job->currentIndex(); // The end storage index of our work. int endIndex = job->endIndex(); unsigned long msgToSelect = mPreSelectionMode == PreSelectLastSelected ? mStorageModel->preSelectedMessage() : 0; MessageItem *mi = nullptr; while (curIndex <= endIndex) { // Create the message item with no parent: we'll set it later if (!mi) { mi = new MessageItem(); } else { // a MessageItem discarded by a previous iteration: reuse it. Q_ASSERT(mi->parent() == nullptr); } if (!mStorageModel->initializeMessageItem(mi, curIndex, bUseReceiver)) { // ugh qCWarning(MESSAGELIST_LOG) << "Fill of the MessageItem at storage row index " << curIndex << " failed"; curIndex++; continue; } // If we're supposed to pre-select a specific message, check if it's this one. if (msgToSelect != 0 && msgToSelect == mi->uniqueId()) { // Found, it's this one. // But actually it's not viewable (so not selectable). We must wait // until the end of the job to be 100% sure. So here we just translate // the unique id to a MessageItem pointer and wait. mLastSelectedMessageInFolder = mi; msgToSelect = 0; // already found, don't bother checking anymore } // Update the newest/oldest message, since we might be supposed to select those later if (mi->date() != static_cast(-1)) { if (!mOldestItem || mOldestItem->date() > mi->date()) { mOldestItem = mi; } if (!mNewestItem || mNewestItem->date() < mi->date()) { mNewestItem = mi; } } // Ok.. it passed the initial checks: we will not be discarding it. // Make this message item an invariant index to the underlying model storage. mInvariantRowMapper->createModelInvariantIndex(curIndex, mi); // Attempt to do threading as soon as possible (to display items to the user) if (mAggregation->threading() != Aggregation::NoThreading) { // Threading is requested // Fetch the data needed for proper threading // Add the item to the threading caches switch (mAggregation->threading()) { case Aggregation::PerfectReferencesAndSubject: mStorageModel->fillMessageItemThreadingData(mi, curIndex, StorageModel::PerfectThreadingReferencesAndSubject); // We also need to build the subject/reference-based threading cache addMessageToReferencesBasedThreadingCache(mi); addMessageToSubjectBasedThreadingCache(mi); break; case Aggregation::PerfectAndReferences: mStorageModel->fillMessageItemThreadingData(mi, curIndex, StorageModel::PerfectThreadingPlusReferences); addMessageToReferencesBasedThreadingCache(mi); break; default: mStorageModel->fillMessageItemThreadingData(mi, curIndex, StorageModel::PerfectThreadingOnly); break; } // Perfect/References threading cache mThreadingCacheMessageIdMD5ToMessageItem.insert(mi->messageIdMD5(), mi); // Register the current item into the threading cache mThreadingCache.addItemToCache(mi); // First of all look into the persistent cache qint64 parentId; Item *pParent = mThreadingCache.parentForItem(mi, parentId); if (pParent) { // We already have the parent MessageItem. Attach current message // to it and mark it as perfect mi->setThreadingStatus(MessageItem::PerfectParentFound); attachMessageToParent(pParent, mi); } else if (parentId > 0) { // We don't have the parent MessageItem yet, but we do know the // parent: delay for pass 2 when we will have the parent MessageItem // for sure. mi->setThreadingStatus(MessageItem::ParentMissing); mUnassignedMessageListForPass2.append(mi); } else if (parentId == 0) { // Message is a thread leader, skip straight to Pass4 mi->setThreadingStatus(MessageItem::NonThreadable); mUnassignedMessageListForPass4.append(mi); } else { // Check if this item is a perfect parent for some imperfectly threaded // message (that is actually attached to it, but not necessarily to the // viewable root). If it is, then remove the imperfect child from its // current parent rebuild the hierarchy on the fly. bool needsImmediateReAttach = false; if (!mThreadingCacheMessageInReplyToIdMD5ToMessageItem.isEmpty()) { // unlikely const auto lImperfectlyThreaded = mThreadingCacheMessageInReplyToIdMD5ToMessageItem.values(mi->messageIdMD5()); for (const auto it : lImperfectlyThreaded) { Q_ASSERT(it->parent()); Q_ASSERT(it->parent() != mi); if (!((it->threadingStatus() == MessageItem::ImperfectParentFound) || (it->threadingStatus() == MessageItem::ParentMissing))) { qCritical() << "Got message " << it << " with threading status" << it->threadingStatus(); Q_ASSERT_X(false, "ModelPrivate::viewItemJobStepInternalForJobPass1Fill", "Wrong threading status"); } // If the item was already attached to the view then // re-attach it immediately. This will avoid a message // being displayed for a short while in the view and then // disappear until a perfect parent isn't found. if (it->isViewable()) { needsImmediateReAttach = true; } it->setThreadingStatus(MessageItem::PerfectParentFound); attachMessageToParent(mi, it); } } // FIXME: Might look by "References" too, here... (?) // Attempt to do threading with anything we already have in caches until now // Note that this is likely to work since thread-parent messages tend // to come before thread-children messages in the folders (simply because of // date of arrival). // First of all try to find a "perfect parent", that is the message for that // we have the ID in the "In-Reply-To" field. This is actually done by using // MD5 caches of the message ids because of speed. Collisions are very unlikely. const QByteArray md5 = mi->inReplyToIdMD5(); if (!md5.isEmpty()) { // Have an In-Reply-To field MD5. // In well behaved mailing lists 70% of the threadable messages get a parent here :) pParent = mThreadingCacheMessageIdMD5ToMessageItem.value(md5, nullptr); if (pParent) { // very likely // Take care of self-referencing (which is always possible) // and circular In-Reply-To reference loops which are possible // in case this item was found to be a perfect parent for some // imperfectly threaded message just above. if ( (mi == pParent) // self referencing message || ( (mi->childItemCount() > 0) // mi already has children, this is fast to determine && pParent->hasAncestor(mi) // pParent is in the mi's children tree ) ) { // Bad, bad message.. it has In-Reply-To equal to Message-Id // or it's in a circular In-Reply-To reference loop. // Will wait for Pass2 with References-Id only qCWarning(MESSAGELIST_LOG) << "Circular In-Reply-To reference loop detected in the message tree"; mUnassignedMessageListForPass2.append(mi); } else { // wow, got a perfect parent for this message! mi->setThreadingStatus(MessageItem::PerfectParentFound); attachMessageToParent(pParent, mi); // we're done with this message (also for Pass2) } } else { // got no parent // will have to wait Pass2 mUnassignedMessageListForPass2.append(mi); } } else { // No In-Reply-To header. bool mightHaveOtherMeansForThreading; switch (mAggregation->threading()) { case Aggregation::PerfectReferencesAndSubject: mightHaveOtherMeansForThreading = mi->subjectIsPrefixed() || !mi->referencesIdMD5().isEmpty(); break; case Aggregation::PerfectAndReferences: mightHaveOtherMeansForThreading = !mi->referencesIdMD5().isEmpty(); break; case Aggregation::PerfectOnly: mightHaveOtherMeansForThreading = false; break; default: // BUG: there shouldn't be other values (NoThreading is excluded in an upper branch) Q_ASSERT(false); mightHaveOtherMeansForThreading = false; // make gcc happy break; } if (mightHaveOtherMeansForThreading) { // We might have other means for threading this message, wait until Pass2 mUnassignedMessageListForPass2.append(mi); } else { // No other means for threading this message. This is either // a standalone message or a thread leader. // If there is no grouping in effect or thread leaders are just the "topmost" // messages then we might be done with this one. if ( (mAggregation->grouping() == Aggregation::NoGrouping) || (mAggregation->threadLeader() == Aggregation::TopmostMessage) ) { // We're done with this message: it will be surely either toplevel (no grouping in effect) // or a thread leader with a well defined group. Do it :) //qCDebug(MESSAGELIST_LOG) << "Setting message status from " << mi->threadingStatus() << " to non threadable (1) " << mi; mi->setThreadingStatus(MessageItem::NonThreadable); // Locate the parent group for this item attachMessageToGroupHeader(mi); // we're done with this message (also for Pass2) } else { // Threads belong to the most recent message in the thread. This means // that we have to wait until Pass2 or Pass3 to assign a group. mUnassignedMessageListForPass2.append(mi); } } } if (needsImmediateReAttach && !mi->isViewable()) { // The item gathered previously viewable children. They must be immediately // re-shown. So this item must currently be attached to the view. // This is a temporary measure: it will be probably still moved. MessageItem *topmost = mi->topmostMessage(); Q_ASSERT(topmost->threadingStatus() == MessageItem::ParentMissing); attachMessageToGroupHeader(topmost); } } } else { // else no threading requested: we don't even need Pass2 // set not threadable status (even if it might be not true, but in this mode we don't care) //qCDebug(MESSAGELIST_LOG) << "Setting message status from " << mi->threadingStatus() << " to non threadable (2) " << mi; mi->setThreadingStatus(MessageItem::NonThreadable); // locate the parent group for this item if (mAggregation->grouping() == Aggregation::NoGrouping) { attachMessageToParent(mRootItem, mi); // no groups requested, attach directly to root } else { attachMessageToGroupHeader(mi); } // we're done with this message (also for Pass2) } mi = nullptr; // this item was pushed somewhere, create a new one at next iteration curIndex++; if ((curIndex % mViewItemJobStepMessageCheckCount) == 0) { if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) { if (curIndex <= endIndex) { job->setCurrentIndex(curIndex); return ViewItemJobInterrupted; } } } } if (mi) { delete mi; } return ViewItemJobCompleted; } ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJobPass1Cleanup(ViewItemJob *job, QElapsedTimer elapsedTimer) { Q_ASSERT(mModelForItemFunctions); // UI must be not disconnected here // In this pass we remove the MessageItem objects that are present in the job // and put their children in the unassigned message list. // Note that this list in fact contains MessageItem objects (we need dynamic_cast<>). QList< ModelInvariantIndex * > *invalidatedMessages = job->invariantIndexList(); // We don't shrink the invalidatedMessages because it's basically an array. // It's faster to traverse an array of N entries than to remove K>0 entries // one by one and to traverse the remaining N-K entries. // The begin index of our work int curIndex = job->currentIndex(); // The end index of our work. int endIndex = job->endIndex(); if (curIndex == job->startIndex()) { Q_ASSERT(mOrphanChildrenHash.isEmpty()); } while (curIndex <= endIndex) { // Get the underlying storage message data... auto dyingMessage = dynamic_cast< MessageItem * >(invalidatedMessages->at(curIndex)); // This MUST NOT be null (otherwise we have a bug somewhere in this file). Q_ASSERT(dyingMessage); // If we were going to pre-select this message but we were interrupted // *before* it was actually made viewable, we just clear the pre-selection pointer // and unique id (abort pre-selection). if (dyingMessage == mLastSelectedMessageInFolder) { mLastSelectedMessageInFolder = nullptr; mPreSelectionMode = PreSelectNone; } // remove the message from any pending user job if (mPersistentSetManager) { mPersistentSetManager->removeMessageItemFromAllSets(dyingMessage); if (mPersistentSetManager->setCount() < 1) { delete mPersistentSetManager; mPersistentSetManager = nullptr; } } // Remove the message from threading cache before we start moving up the // children, so that they don't get mislead by the cache mThreadingCache.expireParent(dyingMessage); if (dyingMessage->parent()) { // Handle saving the current selection: if this item was the current before the step // then zero it out. We have killed it and it's OK for the current item to change. if (dyingMessage == mCurrentItemToRestoreAfterViewItemJobStep) { Q_ASSERT(dyingMessage->isViewable()); // Try to select the item below the removed one as it helps in doing a "readon" of emails: // you read a message, decide to delete it and then go to the next. // Qt tends to select the message above the removed one instead (this is a hardcoded logic in // QItemSelectionModelPrivate::_q_rowsAboutToBeRemoved()). mCurrentItemToRestoreAfterViewItemJobStep = mView->messageItemAfter(dyingMessage, MessageTypeAny, false); if (!mCurrentItemToRestoreAfterViewItemJobStep) { // There is no item below. Try the item above. // We still do it better than qt which tends to find the *thread* above // instead of the item above. mCurrentItemToRestoreAfterViewItemJobStep = mView->messageItemBefore(dyingMessage, MessageTypeAny, false); } Q_ASSERT((!mCurrentItemToRestoreAfterViewItemJobStep) || mCurrentItemToRestoreAfterViewItemJobStep->isViewable()); } if ( dyingMessage->isViewable() && ((dyingMessage)->childItemCount() > 0) // has children && mView->isExpanded(q->index(dyingMessage, 0)) // is actually expanded ) { saveExpandedStateOfSubtree(dyingMessage); } auto oldParent = dyingMessage->parent(); oldParent->takeChildItem(q, dyingMessage); // FIXME: This can generate many message movements.. it would be nicer // to start from messages that are higher in the hierarchy so // we would need to move less stuff above. if (oldParent != mRootItem) { messageDetachedUpdateParentProperties(oldParent, dyingMessage); } // We might have already removed its parent from the view, so it // might already be in the orphan child hash... if (dyingMessage->threadingStatus() == MessageItem::ParentMissing) { mOrphanChildrenHash.remove(dyingMessage); // this can turn to a no-op (dyingMessage not present in fact) } } else { // The dying message had no parent: this should happen only if it's already an orphan Q_ASSERT(dyingMessage->threadingStatus() == MessageItem::ParentMissing); Q_ASSERT(mOrphanChildrenHash.contains(dyingMessage)); Q_ASSERT(dyingMessage != mCurrentItemToRestoreAfterViewItemJobStep); mOrphanChildrenHash.remove(dyingMessage); } if (mAggregation->threading() != Aggregation::NoThreading) { // Threading is requested: remove the message from threading caches. // Remove from the cache of potential parent items mThreadingCacheMessageIdMD5ToMessageItem.remove(dyingMessage->messageIdMD5()); // If we also have a cache for subject/reference-based threading then remove the message from there too if (mAggregation->threading() == Aggregation::PerfectReferencesAndSubject) { removeMessageFromReferencesBasedThreadingCache(dyingMessage); removeMessageFromSubjectBasedThreadingCache(dyingMessage); } else if (mAggregation->threading() == Aggregation::PerfectAndReferences) { removeMessageFromReferencesBasedThreadingCache(dyingMessage); } // If this message wasn't perfectly parented then it might still be in another cache. switch (dyingMessage->threadingStatus()) { case MessageItem::ImperfectParentFound: case MessageItem::ParentMissing: if (!dyingMessage->inReplyToIdMD5().isEmpty()) { mThreadingCacheMessageInReplyToIdMD5ToMessageItem.remove(dyingMessage->inReplyToIdMD5()); } break; default: Q_ASSERT(!mThreadingCacheMessageInReplyToIdMD5ToMessageItem.contains(dyingMessage->inReplyToIdMD5(), dyingMessage)); // make gcc happy break; } } while (auto childItem = dyingMessage->firstChildItem()) { auto childMessage = dynamic_cast< MessageItem * >(childItem); Q_ASSERT(childMessage); dyingMessage->takeChildItem(q, childMessage); if (mAggregation->threading() != Aggregation::NoThreading) { if (childMessage->threadingStatus() == MessageItem::PerfectParentFound) { // If the child message was perfectly parented then now it had // lost its perfect parent. Add to the cache of imperfectly parented. if (!childMessage->inReplyToIdMD5().isEmpty()) { Q_ASSERT(!mThreadingCacheMessageInReplyToIdMD5ToMessageItem.contains(childMessage->inReplyToIdMD5(), childMessage)); mThreadingCacheMessageInReplyToIdMD5ToMessageItem.insert(childMessage->inReplyToIdMD5(), childMessage); } } } // Parent is gone childMessage->setThreadingStatus(MessageItem::ParentMissing); // If the child (or any message in its subtree) is going to be selected, // then we must immediately reattach it to a temporary group in order for the // selection to be preserved across multiple steps. Otherwise we could end // with the child-to-be-selected being non viewable at the end // of the view job step. Attach to a temporary group. if ( // child is going to be re-selected (childMessage == mCurrentItemToRestoreAfterViewItemJobStep) || ( // there is a message that is going to be re-selected mCurrentItemToRestoreAfterViewItemJobStep &&// that message is in the childMessage subtree mCurrentItemToRestoreAfterViewItemJobStep->hasAncestor(childMessage) ) ) { attachMessageToGroupHeader(childMessage); Q_ASSERT(childMessage->isViewable()); } mOrphanChildrenHash.insert(childMessage, childMessage); } if (mNewestItem == dyingMessage) { mNewestItem = nullptr; } if (mOldestItem == dyingMessage) { mOldestItem = nullptr; } delete dyingMessage; curIndex++; // FIXME: Maybe we should check smaller steps here since the // code above can generate large message tree movements // for each single item we sweep in the invalidatedMessages list. if ((curIndex % mViewItemJobStepMessageCheckCount) == 0) { if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) { if (curIndex <= endIndex) { job->setCurrentIndex(curIndex); return ViewItemJobInterrupted; } } } } // We looped over the entire deleted message list. job->setCurrentIndex(endIndex + 1); // A quick last cleaning pass: this is usually very fast so we don't have a real // Pass enumeration for it. We just include it as trailer of Pass1Cleanup to be executed // when job->currentIndex() > job->endIndex(); // We move all the messages from the orphan child hash to the unassigned message // list and get them ready for the standard Pass2. auto it = mOrphanChildrenHash.begin(); auto end = mOrphanChildrenHash.end(); curIndex = 0; while (it != end) { mUnassignedMessageListForPass2.append(*it); it = mOrphanChildrenHash.erase(it); // This is still interruptible curIndex++; // FIXME: We could take "larger" steps here if ((curIndex % mViewItemJobStepMessageCheckCount) == 0) { if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) { if (it != mOrphanChildrenHash.end()) { return ViewItemJobInterrupted; } } } } return ViewItemJobCompleted; } ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJobPass1Update(ViewItemJob *job, QElapsedTimer elapsedTimer) { Q_ASSERT(mModelForItemFunctions); // UI must be not disconnected here // In this pass we simply update the MessageItem objects that are present in the job. // Note that this list in fact contains MessageItem objects (we need dynamic_cast<>). auto messagesThatNeedUpdate = job->invariantIndexList(); // We don't shrink the messagesThatNeedUpdate because it's basically an array. // It's faster to traverse an array of N entries than to remove K>0 entries // one by one and to traverse the remaining N-K entries. // The begin index of our work int curIndex = job->currentIndex(); // The end index of our work. int endIndex = job->endIndex(); while (curIndex <= endIndex) { // Get the underlying storage message data... auto message = dynamic_cast(messagesThatNeedUpdate->at(curIndex)); // This MUST NOT be null (otherwise we have a bug somewhere in this file). Q_ASSERT(message); int row = mInvariantRowMapper->modelInvariantIndexToModelIndexRow(message); if (row < 0) { // Must have been invalidated (so it's basically about to be deleted) Q_ASSERT(!message->isValid()); // Skip it here. curIndex++; continue; } time_t prevDate = message->date(); time_t prevMaxDate = message->maxDate(); bool toDoStatus = message->status().isToAct(); bool prevUnreadStatus = !message->status().isRead(); bool prevImportantStatus = message->status().isImportant(); // The subject/reference based threading cache is sorted by date: we must remove // the item and re-insert it since updateMessageItemData() may change the date too. if (mAggregation->threading() == Aggregation::PerfectReferencesAndSubject) { removeMessageFromReferencesBasedThreadingCache(message); removeMessageFromSubjectBasedThreadingCache(message); } else if (mAggregation->threading() == Aggregation::PerfectAndReferences) { removeMessageFromReferencesBasedThreadingCache(message); } // Do update mStorageModel->updateMessageItemData(message, row); QModelIndex idx = q->index(message, 0); Q_EMIT q->dataChanged(idx, idx); // Reinsert the item to the cache, if needed if (mAggregation->threading() == Aggregation::PerfectReferencesAndSubject) { addMessageToReferencesBasedThreadingCache(message); addMessageToSubjectBasedThreadingCache(message); } else if (mAggregation->threading() == Aggregation::PerfectAndReferences) { addMessageToReferencesBasedThreadingCache(message); } int propertyChangeMask = 0; if (prevDate != message->date()) { propertyChangeMask |= DateChanged; } if (prevMaxDate != message->maxDate()) { propertyChangeMask |= MaxDateChanged; } if (toDoStatus != message->status().isToAct()) { propertyChangeMask |= ActionItemStatusChanged; } if (prevUnreadStatus != (!message->status().isRead())) { propertyChangeMask |= UnreadStatusChanged; } if (prevImportantStatus != (!message->status().isImportant())) { propertyChangeMask |= ImportantStatusChanged; } if (propertyChangeMask) { // Some message data has changed // now we need to handle the changes that might cause re-grouping/re-sorting // and propagate them to the parents. Item *pParent = message->parent(); if (pParent && (pParent != mRootItem)) { // The following function will return true if itemParent may be affected by the change. // If the itemParent isn't affected, we stop climbing. if (handleItemPropertyChanges(propertyChangeMask, pParent, message)) { Q_ASSERT(message->parent()); // handleItemPropertyChanges() must never leave an item detached // Note that actually message->parent() may be different than pParent since // handleItemPropertyChanges() may have re-grouped it. // Time to propagate up. propagateItemPropertiesToParent(message); } } // else there is no parent so the item isn't attached to the view: re-grouping/re-sorting not needed. } // else message data didn't change an there is nothing interesting to do // (re-)apply the filter, if needed if (mFilter && message->isViewable()) { // In all the other cases we (re-)apply the filter to the topmost subtree that this message is in. Item *pTopMostNonRoot = message->topmostNonRoot(); Q_ASSERT(pTopMostNonRoot); Q_ASSERT(pTopMostNonRoot != mRootItem); Q_ASSERT(pTopMostNonRoot->parent() == mRootItem); // FIXME: The call below works, but it's expensive when we are updating // a lot of items with filtering enabled. This is because the updated // items are likely to be in the same subtree which we then filter multiple times. // A point for us is that when filtering there shouldn't be really many // items in the view so the user isn't going to update a lot of them at once... // Well... anyway, the alternative would be to write yet another // specialized routine that would update only the "message" item // above and climb up eventually hiding parents (without descending the sibling subtrees again). // If people complain about performance in this particular case I'll consider that solution. applyFilterToSubtree(pTopMostNonRoot, QModelIndex()); } // otherwise there is no filter or the item isn't viewable: very likely // left detached while propagating property changes. Will filter it // on reattach. // Done updating this message curIndex++; // FIXME: Maybe we should check smaller steps here since the // code above can generate large message tree movements // for each single item we sweep in the messagesThatNeedUpdate list. if ((curIndex % mViewItemJobStepMessageCheckCount) == 0) { if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) { if (curIndex <= endIndex) { job->setCurrentIndex(curIndex); return ViewItemJobInterrupted; } } } } return ViewItemJobCompleted; } ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternalForJob(ViewItemJob *job, QElapsedTimer elapsedTimer) { // This function does a timed chunk of work for a single Fill View job. // It attempts to process messages until a timeout forces it to return to the caller. // A macro would improve readability here but since this is a good point // to place debugger breakpoints then we need it explicitly. // A (template) helper would need to pass many parameters and would not be inlined... if (job->currentPass() == ViewItemJob::Pass1Fill) { // We're in Pass1Fill of the job. switch (viewItemJobStepInternalForJobPass1Fill(job, elapsedTimer)) { case ViewItemJobInterrupted: // current job interrupted by timeout: propagate status to caller return ViewItemJobInterrupted; break; case ViewItemJobCompleted: // pass 1 has been completed // # TODO: Refactor this, make it virtual or whatever, but switch == bad, code duplication etc job->setCurrentPass(ViewItemJob::Pass2); job->setStartIndex(0); job->setEndIndex(mUnassignedMessageListForPass2.count() - 1); // take care of small jobs which never timeout by themselves because // of a small number of messages. At the end of each job check // the time used and if we're timeoutting and there is another job // then interrupt. if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) { return ViewItemJobInterrupted; } // else proceed with the next pass break; default: // This is *really* a BUG qCWarning(MESSAGELIST_LOG) << "ERROR: returned an invalid result"; Q_ASSERT(false); break; } } else if (job->currentPass() == ViewItemJob::Pass1Cleanup) { // We're in Pass1Cleanup of the job. switch (viewItemJobStepInternalForJobPass1Cleanup(job, elapsedTimer)) { case ViewItemJobInterrupted: // current job interrupted by timeout: propagate status to caller return ViewItemJobInterrupted; break; case ViewItemJobCompleted: // pass 1 has been completed job->setCurrentPass(ViewItemJob::Pass2); job->setStartIndex(0); job->setEndIndex(mUnassignedMessageListForPass2.count() - 1); // take care of small jobs which never timeout by themselves because // of a small number of messages. At the end of each job check // the time used and if we're timeoutting and there is another job // then interrupt. if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) { return ViewItemJobInterrupted; } // else proceed with the next pass break; default: // This is *really* a BUG qCWarning(MESSAGELIST_LOG) << "ERROR: returned an invalid result"; Q_ASSERT(false); break; } } else if (job->currentPass() == ViewItemJob::Pass1Update) { // We're in Pass1Update of the job. switch (viewItemJobStepInternalForJobPass1Update(job, elapsedTimer)) { case ViewItemJobInterrupted: // current job interrupted by timeout: propagate status to caller return ViewItemJobInterrupted; break; case ViewItemJobCompleted: // pass 1 has been completed // Since Pass2, Pass3 and Pass4 are empty for an Update operation // we simply skip them. (TODO: Triple-verify this assertion...). job->setCurrentPass(ViewItemJob::Pass5); job->setStartIndex(0); job->setEndIndex(mGroupHeadersThatNeedUpdate.count() - 1); // take care of small jobs which never timeout by themselves because // of a small number of messages. At the end of each job check // the time used and if we're timeoutting and there is another job // then interrupt. if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) { return ViewItemJobInterrupted; } // else proceed with the next pass break; default: // This is *really* a BUG qCWarning(MESSAGELIST_LOG) << "ERROR: returned an invalid result"; Q_ASSERT(false); break; } } // Pass1Fill/Pass1Cleanup/Pass1Update has been already completed. if (job->currentPass() == ViewItemJob::Pass2) { // We're in Pass2 of the job. switch (viewItemJobStepInternalForJobPass2(job, elapsedTimer)) { case ViewItemJobInterrupted: // current job interrupted by timeout: propagate status to caller return ViewItemJobInterrupted; break; case ViewItemJobCompleted: // pass 2 has been completed job->setCurrentPass(ViewItemJob::Pass3); job->setStartIndex(0); job->setEndIndex(mUnassignedMessageListForPass3.count() - 1); // take care of small jobs which never timeout by themselves because // of a small number of messages. At the end of each job check // the time used and if we're timeoutting and there is another job // then interrupt. if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) { return ViewItemJobInterrupted; } // else proceed with the next pass break; default: // This is *really* a BUG qCWarning(MESSAGELIST_LOG) << "ERROR: returned an invalid result"; Q_ASSERT(false); break; } } if (job->currentPass() == ViewItemJob::Pass3) { // We're in Pass3 of the job. switch (viewItemJobStepInternalForJobPass3(job, elapsedTimer)) { case ViewItemJobInterrupted: // current job interrupted by timeout: propagate status to caller return ViewItemJobInterrupted; case ViewItemJobCompleted: // pass 3 has been completed job->setCurrentPass(ViewItemJob::Pass4); job->setStartIndex(0); job->setEndIndex(mUnassignedMessageListForPass4.count() - 1); // take care of small jobs which never timeout by themselves because // of a small number of messages. At the end of each job check // the time used and if we're timeoutting and there is another job // then interrupt. if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) { return ViewItemJobInterrupted; } // else proceed with the next pass break; default: // This is *really* a BUG qCWarning(MESSAGELIST_LOG) << "ERROR: returned an invalid result"; Q_ASSERT(false); break; } } if (job->currentPass() == ViewItemJob::Pass4) { // We're in Pass4 of the job. switch (viewItemJobStepInternalForJobPass4(job, elapsedTimer)) { case ViewItemJobInterrupted: // current job interrupted by timeout: propagate status to caller return ViewItemJobInterrupted; case ViewItemJobCompleted: // pass 4 has been completed job->setCurrentPass(ViewItemJob::Pass5); job->setStartIndex(0); job->setEndIndex(mGroupHeadersThatNeedUpdate.count() - 1); // take care of small jobs which never timeout by themselves because // of a small number of messages. At the end of each job check // the time used and if we're timeoutting and there is another job // then interrupt. if (elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) { return ViewItemJobInterrupted; } // else proceed with the next pass break; default: // This is *really* a BUG qCWarning(MESSAGELIST_LOG) << "ERROR: returned an invalid result"; Q_ASSERT(false); break; } } // Pass4 has been already completed. Proceed to Pass5. return viewItemJobStepInternalForJobPass5(job, elapsedTimer); } #ifdef KDEPIM_FOLDEROPEN_PROFILE // Namespace to collect all the vars and functions for KDEPIM_FOLDEROPEN_PROFILE namespace Stats { // Number of existing jobs/passes static const int numberOfPasses = ViewItemJob::LastIndex; // The pass in the last call of viewItemJobStepInternal(), used to detect when // a new pass starts static int lastPass = -1; // Total number of messages in the folder static int totalMessages; // Per-Job data static int numElements[numberOfPasses]; static int totalTime[numberOfPasses]; static int chunks[numberOfPasses]; // Time, in msecs for some special operations static int expandingTreeTime; static int layoutChangeTime; // Descriptions of the job, for nicer debug output static const char *jobDescription[numberOfPasses] = { "Creating items from messages and simple threading", "Removing messages", "Updating messages", "Additional Threading", "Subject-Based threading", "Grouping", "Group resorting + cleanup" }; // Timer to track time between start of first job and end of last job static QTime firstStartTime; // Timer to track time the current job takes static QTime currentJobStartTime; // Zeros the stats, to be called when the first job starts static void resetStats() { totalMessages = 0; layoutChangeTime = 0; expandingTreeTime = 0; lastPass = -1; for (int i = 0; i < numberOfPasses; ++i) { numElements[i] = 0; totalTime[i] = 0; chunks[i] = 0; } } } // namespace Stats void ModelPrivate::printStatistics() { using namespace Stats; int totalTotalTime = 0; int completeTime = firstStartTime.elapsed(); for (int i = 0; i < numberOfPasses; ++i) { totalTotalTime += totalTime[i]; } float msgPerSecond = totalMessages / (totalTotalTime / 1000.0f); float msgPerSecondComplete = totalMessages / (completeTime / 1000.0f); int messagesWithSameSubjectAvg = 0; int messagesWithSameSubjectMax = 0; for (const auto messages : qAsConst(mThreadingCacheMessageSubjectMD5ToMessageItem)) { if (messages->size() > messagesWithSameSubjectMax) { messagesWithSameSubjectMax = messages->size(); } messagesWithSameSubjectAvg += messages->size(); } messagesWithSameSubjectAvg = messagesWithSameSubjectAvg / (float)mThreadingCacheMessageSubjectMD5ToMessageItem.size(); int totalThreads = 0; if (!mGroupHeaderItemHash.isEmpty()) { foreach (const GroupHeaderItem *groupHeader, mGroupHeaderItemHash) { totalThreads += groupHeader->childItemCount(); } } else { totalThreads = mRootItem->childItemCount(); } qCDebug(MESSAGELIST_LOG) << "Finished filling the view with" << totalMessages << "messages"; qCDebug(MESSAGELIST_LOG) << "That took" << totalTotalTime << "msecs inside the model and" << completeTime << "in total."; qCDebug(MESSAGELIST_LOG) << (totalTotalTime / (float)completeTime) * 100.0f << "percent of the time was spent in the model."; qCDebug(MESSAGELIST_LOG) << "Time for layoutChanged(), in msecs:" << layoutChangeTime << "(" << (layoutChangeTime / (float)totalTotalTime) * 100.0f << "percent )"; qCDebug(MESSAGELIST_LOG) << "Time to expand tree, in msecs:" << expandingTreeTime << "(" << (expandingTreeTime / (float)totalTotalTime) * 100.0f << "percent )"; qCDebug(MESSAGELIST_LOG) << "Number of messages per second in the model:" << msgPerSecond; qCDebug(MESSAGELIST_LOG) << "Number of messages per second in total:" << msgPerSecondComplete; qCDebug(MESSAGELIST_LOG) << "Number of threads:" << totalThreads; qCDebug(MESSAGELIST_LOG) << "Number of groups:" << mGroupHeaderItemHash.size(); qCDebug(MESSAGELIST_LOG) << "Messages per thread:" << totalMessages / (float)totalThreads; qCDebug(MESSAGELIST_LOG) << "Threads per group:" << totalThreads / (float)mGroupHeaderItemHash.size(); qCDebug(MESSAGELIST_LOG) << "Messages with the same subject:" << "Max:" << messagesWithSameSubjectMax << "Avg:" << messagesWithSameSubjectAvg; qCDebug(MESSAGELIST_LOG); qCDebug(MESSAGELIST_LOG) << "Now follows a breakdown of the jobs."; qCDebug(MESSAGELIST_LOG); for (int i = 0; i < numberOfPasses; ++i) { if (totalTime[i] == 0) { continue; } float elementsPerSecond = numElements[i] / (totalTime[i] / 1000.0f); float percent = totalTime[i] / (float)totalTotalTime * 100.0f; qCDebug(MESSAGELIST_LOG) << "----------------------------------------------"; qCDebug(MESSAGELIST_LOG) << "Job" << i + 1 << "(" << jobDescription[i] << ")"; qCDebug(MESSAGELIST_LOG) << "Share of complete time:" << percent << "percent"; qCDebug(MESSAGELIST_LOG) << "Time in msecs:" << totalTime[i]; qCDebug(MESSAGELIST_LOG) << "Number of elements:" << numElements[i]; // TODO: map of element string qCDebug(MESSAGELIST_LOG) << "Elements per second:" << elementsPerSecond; qCDebug(MESSAGELIST_LOG) << "Number of chunks:" << chunks[i]; qCDebug(MESSAGELIST_LOG); } qCDebug(MESSAGELIST_LOG) << "=========================================================="; resetStats(); } #endif ModelPrivate::ViewItemJobResult ModelPrivate::viewItemJobStepInternal() { // This function does a timed chunk of work in our View Fill operation. // It attempts to do processing until it either runs out of jobs // to be done or a timeout forces it to interrupt and jump back to the caller. QElapsedTimer elapsedTimer; elapsedTimer.start(); while (!mViewItemJobs.isEmpty()) { // Have a job to do. - ViewItemJob *job = mViewItemJobs.first(); + ViewItemJob *job = mViewItemJobs.constFirst(); #ifdef KDEPIM_FOLDEROPEN_PROFILE // Here we check if an old job has just completed or if we are at the start of the // first job. We then initialize job data stuff and timers based on this. const int currentPass = job->currentPass(); const bool firstChunk = currentPass != Stats::lastPass; if (currentPass != Stats::lastPass && Stats::lastPass != -1) { Stats::totalTime[Stats::lastPass] = Stats::currentJobStartTime.elapsed(); } const bool firstJob = job->currentPass() == ViewItemJob::Pass1Fill && firstChunk; const int elements = job->endIndex() - job->startIndex(); if (firstJob) { Stats::resetStats(); Stats::totalMessages = elements; Stats::firstStartTime.restart(); } if (firstChunk) { Stats::numElements[currentPass] = elements; Stats::currentJobStartTime.restart(); } Stats::chunks[currentPass]++; Stats::lastPass = currentPass; #endif mViewItemJobStepIdleInterval = job->idleInterval(); mViewItemJobStepChunkTimeout = job->chunkTimeout(); mViewItemJobStepMessageCheckCount = job->messageCheckCount(); if (job->disconnectUI()) { mModelForItemFunctions = nullptr; // disconnect the UI for this job Q_ASSERT(mLoading); // this must be true in the first job // FIXME: Should assert yet more that this is the very first job for this StorageModel // Asserting only mLoading is not enough as we could be using a two-jobs loading strategy // or this could be a job enqueued before the first job has completed. } else { // With a connected UI we need to avoid the view to update the scrollbars at EVERY insertion or expansion. // QTreeViewPrivate::updateScrollBars() is very expensive as it loops through ALL the items in the view every time. // We can't disable the function directly as it's hidden in the private data object of QTreeView // but we can disable the parent QTreeView::updateGeometries() instead. // We will trigger it "manually" at the end of the step. mView->ignoreUpdateGeometries(true); // Ok.. I know that this seems unbelieveable but disabling updates actually // causes a (significant) performance loss in most cases. This is probably because QTreeView // uses delayed layouts when updates are disabled which should be delayed but in // fact are "forced" by next item insertions. The delayed layout algorithm, then // is probably slower than the non-delayed one. // Disabling the paintEvent() doesn't seem to work either. //mView->setUpdatesEnabled( false ); } switch (viewItemJobStepInternalForJob(job, elapsedTimer)) { case ViewItemJobInterrupted: // current job interrupted by timeout: will propagate status to caller // but before this, give some feedback to the user // FIXME: This is now inaccurate, think of something else switch (job->currentPass()) { case ViewItemJob::Pass1Fill: case ViewItemJob::Pass1Cleanup: case ViewItemJob::Pass1Update: Q_EMIT q->statusMessage(i18np("Processed 1 Message of %2", "Processed %1 Messages of %2", job->currentIndex() - job->startIndex(), job->endIndex() - job->startIndex() + 1)); break; case ViewItemJob::Pass2: Q_EMIT q->statusMessage(i18np("Threaded 1 Message of %2", "Threaded %1 Messages of %2", job->currentIndex() - job->startIndex(), job->endIndex() - job->startIndex() + 1)); break; case ViewItemJob::Pass3: Q_EMIT q->statusMessage(i18np("Threaded 1 Message of %2", "Threaded %1 Messages of %2", job->currentIndex() - job->startIndex(), job->endIndex() - job->startIndex() + 1)); break; case ViewItemJob::Pass4: Q_EMIT q->statusMessage(i18np("Grouped 1 Thread of %2", "Grouped %1 Threads of %2", job->currentIndex() - job->startIndex(), job->endIndex() - job->startIndex() + 1)); break; case ViewItemJob::Pass5: Q_EMIT q->statusMessage(i18np("Updated 1 Group of %2", "Updated %1 Groups of %2", job->currentIndex() - job->startIndex(), job->endIndex() - job->startIndex() + 1)); break; default: break; } if (!job->disconnectUI()) { mView->ignoreUpdateGeometries(false); // explicit call to updateGeometries() here mView->updateGeometries(); } return ViewItemJobInterrupted; break; case ViewItemJobCompleted: // If this job worked with a disconnected UI, Q_EMIT layoutChanged() // to reconnect it. We go back to normal operation now. if (job->disconnectUI()) { mModelForItemFunctions = q; // This call would destroy the expanded state of items. // This is why when mModelForItemFunctions was 0 we didn't actually expand them // but we just set a "ExpandNeeded" mark... #ifdef KDEPIM_FOLDEROPEN_PROFILE QTime layoutChangedTimer; layoutChangedTimer.start(); #endif mView->modelAboutToEmitLayoutChanged(); Q_EMIT q->layoutChanged(); mView->modelEmittedLayoutChanged(); #ifdef KDEPIM_FOLDEROPEN_PROFILE Stats::layoutChangeTime = layoutChangedTimer.elapsed(); QTime expandingTime; expandingTime.start(); #endif // expand all the items that need it in a single sweep // FIXME: This takes quite a lot of time, it could be made an interruptible job auto rootChildItems = mRootItem->childItems(); if (rootChildItems) { for (const auto it : qAsConst(*rootChildItems)) { if (it->initialExpandStatus() == Item::ExpandNeeded) { syncExpandedStateOfSubtree(it); } } } #ifdef KDEPIM_FOLDEROPEN_PROFILE Stats::expandingTreeTime = expandingTime.elapsed(); #endif } else { mView->ignoreUpdateGeometries(false); // explicit call to updateGeometries() here mView->updateGeometries(); } // this job has been completed delete mViewItemJobs.takeFirst(); #ifdef KDEPIM_FOLDEROPEN_PROFILE // Last job finished! Stats::totalTime[currentPass] = Stats::currentJobStartTime.elapsed(); printStatistics(); #endif // take care of small jobs which never timeout by themselves because // of a small number of messages. At the end of each job check // the time used and if we're timeoutting and there is another job // then interrupt. if ((elapsedTimer.elapsed() > mViewItemJobStepChunkTimeout) || (elapsedTimer.elapsed() < 0)) { if (!mViewItemJobs.isEmpty()) { return ViewItemJobInterrupted; } // else it's completed in fact } // else proceed with the next job break; default: // This is *really* a BUG qCWarning(MESSAGELIST_LOG) << "ERROR: returned an invalid result"; Q_ASSERT(false); break; } } // no more jobs Q_EMIT q->statusMessage(i18nc("@info:status Finished view fill", "Ready")); return ViewItemJobCompleted; } void ModelPrivate::viewItemJobStep() { // A single step in the View Fill operation. // This function wraps viewItemJobStepInternal() which does the step job // and either completes it or stops because of a timeout. // If the job is stopped then we start a zero-msecs timer to call us // back and resume the job. Otherwise we're just done. mViewItemJobStepStartTime = ::time(nullptr); if (mFillStepTimer.isActive()) { mFillStepTimer.stop(); } if (!mStorageModel) { return; // nothing more to do } // Save the current item in the view as our process may // cause items to be reparented (and QTreeView will forget the current item in the meantime). // This machinery is also needed when we're about to remove items from the view in // a cleanup job: we'll be trying to set as current the item after the one removed. QModelIndex currentIndexBeforeStep = mView->currentIndex(); Item *currentItemBeforeStep = currentIndexBeforeStep.isValid() ? static_cast< Item * >(currentIndexBeforeStep.internalPointer()) : nullptr; // mCurrentItemToRestoreAfterViewItemJobStep will be zeroed out if it's killed mCurrentItemToRestoreAfterViewItemJobStep = currentItemBeforeStep; // Save the current item position in the viewport as QTreeView fails to keep // the current item in the sample place when items are added or removed... QRect rectBeforeViewItemJobStep; const bool lockView = mView->isScrollingLocked(); // This is generally SLOW AS HELL... (so we avoid it if we lock the view and thus don't need it) if (mCurrentItemToRestoreAfterViewItemJobStep && (!lockView)) { rectBeforeViewItemJobStep = mView->visualRect(currentIndexBeforeStep); } // FIXME: If the current item is NOT in the view, preserve the position // of the top visible item. This will make the view move yet less. // Insulate the View from (very likely spurious) "currentChanged()" signals. mView->ignoreCurrentChanges(true); // And go to real work. switch (viewItemJobStepInternal()) { case ViewItemJobInterrupted: // Operation timed out, need to resume in a while if (!mInLengthyJobBatch) { mInLengthyJobBatch = true; } mFillStepTimer.start(mViewItemJobStepIdleInterval); // this is a single shot timer connected to viewItemJobStep() // and go dealing with current/selection out of the switch. break; case ViewItemJobCompleted: // done :) Q_ASSERT(mModelForItemFunctions); // UI must be no (longer) disconnected in this state // Ask the view to remove the eventual busy indications if (mInLengthyJobBatch) { mInLengthyJobBatch = false; } if (mLoading) { mLoading = false; mView->modelFinishedLoading(); } // Apply pre-selection, if any if (mPreSelectionMode != PreSelectNone) { mView->ignoreCurrentChanges(false); bool bSelectionDone = false; switch (mPreSelectionMode) { case PreSelectLastSelected: // fall down break; case PreSelectFirstUnreadCentered: bSelectionDone = mView->selectFirstMessageItem(MessageTypeUnreadOnly, true); // center break; case PreSelectOldestCentered: mView->setCurrentMessageItem(mOldestItem, true /* center */); bSelectionDone = true; break; case PreSelectNewestCentered: mView->setCurrentMessageItem(mNewestItem, true /* center */); bSelectionDone = true; break; case PreSelectNone: // deal with selection below break; default: qCWarning(MESSAGELIST_LOG) << "ERROR: Unrecognized pre-selection mode " << static_cast(mPreSelectionMode); break; } if ((!bSelectionDone) && (mPreSelectionMode != PreSelectNone)) { // fallback to last selected, if possible if (mLastSelectedMessageInFolder) { // we found it in the loading process: select and jump out mView->setCurrentMessageItem(mLastSelectedMessageInFolder); bSelectionDone = true; } } if (bSelectionDone) { mLastSelectedMessageInFolder = nullptr; mPreSelectionMode = PreSelectNone; return; // already taken care of current / selection } } // deal with current/selection out of the switch break; default: // This is *really* a BUG qCWarning(MESSAGELIST_LOG) << "ERROR: returned an invalid result"; Q_ASSERT(false); break; } // Everything else here deals with the selection // If UI is disconnected then we don't have anything else to do here if (!mModelForItemFunctions) { mView->ignoreCurrentChanges(false); return; } // Restore current/selection and/or scrollbar position if (mCurrentItemToRestoreAfterViewItemJobStep) { bool stillIgnoringCurrentChanges = true; // If the assert below fails then the previously current item got detached // and didn't get reattached in the step: this should never happen. Q_ASSERT(mCurrentItemToRestoreAfterViewItemJobStep->isViewable()); // Check if the current item changed QModelIndex currentIndexAfterStep = mView->currentIndex(); Item *currentAfterStep = currentIndexAfterStep.isValid() ? static_cast< Item * >(currentIndexAfterStep.internalPointer()) : nullptr; if (mCurrentItemToRestoreAfterViewItemJobStep != currentAfterStep) { // QTreeView lost the current item... if (mCurrentItemToRestoreAfterViewItemJobStep != currentItemBeforeStep) { // Some view job code expects us to actually *change* the current item. // This is done by the cleanup step which removes items and tries // to set as current the item *after* the removed one, if possible. // We need the view to handle the change though. stillIgnoringCurrentChanges = false; mView->ignoreCurrentChanges(false); } else { // we just have to restore the old current item. The code // outside shouldn't have noticed that we lost it (e.g. the message viewer // still should have the old message opened). So we don't need to // actually notify the view of the restored setting. } // Restore it qCDebug(MESSAGELIST_LOG) << "Gonna restore current here" << mCurrentItemToRestoreAfterViewItemJobStep->subject(); mView->setCurrentIndex(q->index(mCurrentItemToRestoreAfterViewItemJobStep, 0)); } else { // The item we're expected to set as current is already current if (mCurrentItemToRestoreAfterViewItemJobStep != currentItemBeforeStep) { // But we have changed it in the job step. // This means that: we have deleted the current item and chosen a // new candidate as current but Qt also has chosen it as candidate // and already made it current. The problem is that (as of Qt 4.4) // it probably didn't select it. if (!mView->selectionModel()->hasSelection()) { stillIgnoringCurrentChanges = false; mView->ignoreCurrentChanges(false); qCDebug(MESSAGELIST_LOG) << "Gonna restore selection here" << mCurrentItemToRestoreAfterViewItemJobStep->subject(); QItemSelection selection; selection.append(QItemSelectionRange(q->index(mCurrentItemToRestoreAfterViewItemJobStep, 0))); mView->selectionModel()->select(selection, QItemSelectionModel::Select | QItemSelectionModel::Rows); } } } // FIXME: If it was selected before the change, then re-select it (it may happen that it's not) if (!lockView) { // we prefer to keep the currently selected item steady in the view QRect rectAfterViewItemJobStep = mView->visualRect(q->index(mCurrentItemToRestoreAfterViewItemJobStep, 0)); if (rectBeforeViewItemJobStep.y() != rectAfterViewItemJobStep.y()) { // QTreeView lost its position... mView->verticalScrollBar()->setValue(mView->verticalScrollBar()->value() + rectAfterViewItemJobStep.y() - rectBeforeViewItemJobStep.y()); } } // and kill the insulation, if not yet done if (stillIgnoringCurrentChanges) { mView->ignoreCurrentChanges(false); } return; } // Either there was no current item before, or it was lost in a cleanup step and another candidate for // current item couldn't be found (possibly empty view) mView->ignoreCurrentChanges(false); if (currentItemBeforeStep) { // lost in a cleanup.. // tell the view that we have a new current, this time with no insulation mView->slotSelectionChanged(QItemSelection(), QItemSelection()); } } void ModelPrivate::slotStorageModelRowsInserted(const QModelIndex &parent, int from, int to) { if (parent.isValid()) { return; // ugh... should never happen } Q_ASSERT(from <= to); int count = (to - from) + 1; mInvariantRowMapper->modelRowsInserted(from, count); // look if no current job is in the middle int jobCount = mViewItemJobs.count(); for (int idx = 0; idx < jobCount; idx++) { ViewItemJob *job = mViewItemJobs.at(idx); if (job->currentPass() != ViewItemJob::Pass1Fill) { // The job is a cleanup or in a later pass: the storage has been already accessed // and the messages created... no need to care anymore: the invariant row mapper will do the job. continue; } if (job->currentIndex() > job->endIndex()) { // The job finished the Pass1Fill but still waits for the pass indicator to be // changed. This is unlikely but still may happen if the job has been interrupted // and then a call to slotStorageModelRowsRemoved() caused it to be forcibly completed. continue; } // // The following cases are possible: // // from to // | | -> shift up job // from to // | | -> shift up job // from to // | | -> shift up job // from to // | | -> split job // from to // | | -> split job // from to // | | -> job unaffected // // // FOLDER // |-------------------------|---------|--------------| // 0 currentIndex endIndex count // +-- job --+ // if (from > job->endIndex()) { // The change is completely above the job, the job is not affected continue; } if (from > job->currentIndex()) { // and from <= job->endIndex() // The change starts in the middle of the job in a way that it must be split in two. // The first part is unaffected by the shift and ranges from job->currentIndex() to from - 1. // The second part ranges from "from" to job->endIndex() that are now shifted up by count steps. // First add a new job for the second part. auto newJob = new ViewItemJob(from + count, job->endIndex() + count, job->chunkTimeout(), job->idleInterval(), job->messageCheckCount()); Q_ASSERT(newJob->currentIndex() <= newJob->endIndex()); idx++; // we can skip this job in the loop, it's already ok jobCount++; // and our range increases by one. mViewItemJobs.insert(idx, newJob); // Then limit the original job to the first part job->setEndIndex(from - 1); Q_ASSERT(job->currentIndex() <= job->endIndex()); continue; } // The change starts below (or exactly on the beginning of) the job. // The job must be shifted up. job->setCurrentIndex(job->currentIndex() + count); job->setEndIndex(job->endIndex() + count); Q_ASSERT(job->currentIndex() <= job->endIndex()); } bool newJobNeeded = true; // Try to attach to an existing fill job, if any. // To enforce consistency we can attach only if the Fill job // is the last one in the list (might be eventually *also* the first, // and even being already processed but we must make sure that there // aren't jobs _after_ it). if (jobCount > 0) { ViewItemJob *job = mViewItemJobs.at(jobCount - 1); if (job->currentPass() == ViewItemJob::Pass1Fill) { if ( // The job ends just before the added rows (from == (job->endIndex() + 1)) &&// The job didn't reach the end of Pass1Fill yet (job->currentIndex() <= job->endIndex()) ) { // We can still attach this :) job->setEndIndex(to); Q_ASSERT(job->currentIndex() <= job->endIndex()); newJobNeeded = false; } } } if (newJobNeeded) { // FIXME: Should take timing options from aggregation here ? ViewItemJob *job = new ViewItemJob(from, to, 100, 50, 10); mViewItemJobs.append(job); } if (!mFillStepTimer.isActive()) { mFillStepTimer.start(mViewItemJobStepIdleInterval); } } void ModelPrivate::slotStorageModelRowsRemoved(const QModelIndex &parent, int from, int to) { // This is called when the underlying StorageModel emits the rowsRemoved signal. if (parent.isValid()) { return; // ugh... should never happen } // look if no current job is in the middle Q_ASSERT(from <= to); const int count = (to - from) + 1; int jobCount = mViewItemJobs.count(); if (mRootItem && from == 0 && count == mRootItem->childItemCount() && jobCount == 0) { clear(); return; } for (int idx = 0; idx < jobCount; idx++) { ViewItemJob *job = mViewItemJobs.at(idx); if (job->currentPass() != ViewItemJob::Pass1Fill) { // The job is a cleanup or in a later pass: the storage has been already accessed // and the messages created... no need to care: we will invalidate the messages in a while. continue; } if (job->currentIndex() > job->endIndex()) { // The job finished the Pass1Fill but still waits for the pass indicator to be // changed. This is unlikely but still may happen if the job has been interrupted // and then a call to slotStorageModelRowsRemoved() caused it to be forcibly completed. continue; } // // The following cases are possible: // // from to // | | -> shift down job // from to // | | -> shift down and crop job // from to // | | -> kill job // from to // | | -> split job, crop and shift // from to // | | -> crop job // from to // | | -> job unaffected // // // FOLDER // |-------------------------|---------|--------------| // 0 currentIndex endIndex count // +-- job --+ // if (from > job->endIndex()) { // The change is completely above the job, the job is not affected continue; } if (from > job->currentIndex()) { // and from <= job->endIndex() // The change starts in the middle of the job and ends in the middle or after the job. // The first part is unaffected by the shift and ranges from job->currentIndex() to from - 1 // We use the existing job for this. job->setEndIndex(from - 1); // stop before the first removed row Q_ASSERT(job->currentIndex() <= job->endIndex()); if (to < job->endIndex()) { // The change ends inside the job and a part of it can be completed. // We create a new job for the shifted remaining part. It would actually // range from to + 1 up to job->endIndex(), but we need to shift it down by count. // since count = ( to - from ) + 1 so from = to + 1 - count auto newJob = new ViewItemJob(from, job->endIndex() - count, job->chunkTimeout(), job->idleInterval(), job->messageCheckCount()); Q_ASSERT(newJob->currentIndex() < newJob->endIndex()); idx++; // we can skip this job in the loop, it's already ok jobCount++; // and our range increases by one. mViewItemJobs.insert(idx, newJob); } // else the change includes completely the end of the job and no other part of it can be completed. continue; } // The change starts below (or exactly on the beginning of) the job. ( from <= job->currentIndex() ) if (to >= job->endIndex()) { // The change completely covers the job: kill it // We don't delete the job since we want the other passes to be completed // This is because the Pass1Fill may have already filled mUnassignedMessageListForPass2 // and may have set mOldestItem and mNewestItem. We *COULD* clear the unassigned // message list with clearUnassignedMessageLists() but mOldestItem and mNewestItem // could be still dangling pointers. So we just move the current index of the job // after the end (so storage model scan terminates) and let it complete spontaneously. job->setCurrentIndex(job->endIndex() + 1); continue; } if (to >= job->currentIndex()) { // The change partially covers the job. Only a part of it can be completed // and it must be shifted down. It would actually // range from to + 1 up to job->endIndex(), but we need to shift it down by count. // since count = ( to - from ) + 1 so from = to + 1 - count job->setCurrentIndex(from); job->setEndIndex(job->endIndex() - count); Q_ASSERT(job->currentIndex() <= job->endIndex()); continue; } // The change is completely below the job: it must be shifted down. job->setCurrentIndex(job->currentIndex() - count); job->setEndIndex(job->endIndex() - count); } // This will invalidate the ModelInvariantIndex-es that have been removed and return // them all in a nice list that we can feed to a view removal job. auto invalidatedIndexes = mInvariantRowMapper->modelRowsRemoved(from, count); if (invalidatedIndexes) { // Try to attach to an existing cleanup job, if any. // To enforce consistency we can attach only if the Cleanup job // is the last one in the list (might be eventually *also* the first, // and even being already processed but we must make sure that there // aren't jobs _after_ it). if (jobCount > 0) { ViewItemJob *job = mViewItemJobs.at(jobCount - 1); if (job->currentPass() == ViewItemJob::Pass1Cleanup) { if ((job->currentIndex() <= job->endIndex()) && job->invariantIndexList()) { //qCDebug(MESSAGELIST_LOG) << "Appending " << invalidatedIndexes->count() << " invalidated indexes to existing cleanup job"; // We can still attach this :) *(job->invariantIndexList()) += *invalidatedIndexes; job->setEndIndex(job->endIndex() + invalidatedIndexes->count()); delete invalidatedIndexes; invalidatedIndexes = nullptr; } } } if (invalidatedIndexes) { // Didn't append to any existing cleanup job.. create a new one //qCDebug(MESSAGELIST_LOG) << "Creating new cleanup job for " << invalidatedIndexes->count() << " invalidated indexes"; // FIXME: Should take timing options from aggregation here ? auto job = new ViewItemJob(ViewItemJob::Pass1Cleanup, invalidatedIndexes, 100, 50, 10); mViewItemJobs.append(job); } if (!mFillStepTimer.isActive()) { mFillStepTimer.start(mViewItemJobStepIdleInterval); } } } void ModelPrivate::slotStorageModelLayoutChanged() { qCDebug(MESSAGELIST_LOG) << "Storage model layout changed"; // need to reset everything... q->setStorageModel(mStorageModel); qCDebug(MESSAGELIST_LOG) << "Storage model layout changed done"; } void ModelPrivate::slotStorageModelDataChanged(const QModelIndex &fromIndex, const QModelIndex &toIndex) { Q_ASSERT(mStorageModel); // must exist (and be the sender of the signal connected to this slot) int from = fromIndex.row(); int to = toIndex.row(); Q_ASSERT(from <= to); int count = (to - from) + 1; int jobCount = mViewItemJobs.count(); // This will find out the ModelInvariantIndex-es that need an update and will return // them all in a nice list that we can feed to a view removal job. auto indexesThatNeedUpdate = mInvariantRowMapper->modelIndexRowRangeToModelInvariantIndexList(from, count); if (indexesThatNeedUpdate) { // Try to attach to an existing update job, if any. // To enforce consistency we can attach only if the Update job // is the last one in the list (might be eventually *also* the first, // and even being already processed but we must make sure that there // aren't jobs _after_ it). if (jobCount > 0) { ViewItemJob *job = mViewItemJobs.at(jobCount - 1); if (job->currentPass() == ViewItemJob::Pass1Update) { if ((job->currentIndex() <= job->endIndex()) && job->invariantIndexList()) { // We can still attach this :) *(job->invariantIndexList()) += *indexesThatNeedUpdate; job->setEndIndex(job->endIndex() + indexesThatNeedUpdate->count()); delete indexesThatNeedUpdate; indexesThatNeedUpdate = nullptr; } } } if (indexesThatNeedUpdate) { // Didn't append to any existing update job.. create a new one // FIXME: Should take timing options from aggregation here ? auto job = new ViewItemJob(ViewItemJob::Pass1Update, indexesThatNeedUpdate, 100, 50, 10); mViewItemJobs.append(job); } if (!mFillStepTimer.isActive()) { mFillStepTimer.start(mViewItemJobStepIdleInterval); } } } void ModelPrivate::slotStorageModelHeaderDataChanged(Qt::Orientation, int, int) { if (mStorageModelContainsOutboundMessages != mStorageModel->containsOutboundMessages()) { mStorageModelContainsOutboundMessages = mStorageModel->containsOutboundMessages(); Q_EMIT q->headerDataChanged(Qt::Horizontal, 0, q->columnCount()); } } Qt::ItemFlags Model::flags(const QModelIndex &index) const { if (!index.isValid()) { return Qt::NoItemFlags; } Q_ASSERT(d->mModelForItemFunctions); // UI must be connected if a valid index was queried Item *it = static_cast< Item * >(index.internalPointer()); Q_ASSERT(it); if (it->type() == Item::GroupHeader) { return Qt::ItemIsEnabled; } Q_ASSERT(it->type() == Item::Message); if (!static_cast< MessageItem * >(it)->isValid()) { return Qt::NoItemFlags; // not enabled, not selectable } if (static_cast< MessageItem * >(it)->aboutToBeRemoved()) { return Qt::NoItemFlags; // not enabled, not selectable } if (static_cast< MessageItem * >(it)->status().isDeleted()) { return Qt::NoItemFlags; // not enabled, not selectable } return Qt::ItemIsEnabled | Qt::ItemIsSelectable; } QMimeData *MessageList::Core::Model::mimeData(const QModelIndexList &indexes) const { QVector< MessageItem * > msgs; for (const QModelIndex &idx : indexes) { if (idx.isValid()) { Item *item = static_cast(idx.internalPointer()); if (item->type() == MessageList::Core::Item::Message) { msgs << static_cast(idx.internalPointer()); } } } return storageModel()->mimeData(msgs); } Item *Model::rootItem() const { return d->mRootItem; } bool Model::isLoading() const { return d->mLoading; } MessageItem *Model::messageItemByStorageRow(int row) const { if (!d->mStorageModel) { return nullptr; } auto idx = d->mInvariantRowMapper->modelIndexRowToModelInvariantIndex(row); if (!idx) { return nullptr; } return static_cast< MessageItem * >(idx); } MessageItemSetReference Model::createPersistentSet(const QVector &items) { if (!d->mPersistentSetManager) { d->mPersistentSetManager = new MessageItemSetManager(); } MessageItemSetReference ref = d->mPersistentSetManager->createSet(); for (const auto mi : items) { d->mPersistentSetManager->addMessageItem(ref, mi); } return ref; } QList Model::persistentSetCurrentMessageItemList(MessageItemSetReference ref) { if (d->mPersistentSetManager) { return d->mPersistentSetManager->messageItems(ref); } return QList< MessageItem * >(); } void Model::deletePersistentSet(MessageItemSetReference ref) { if (!d->mPersistentSetManager) { return; } d->mPersistentSetManager->removeSet(ref); if (d->mPersistentSetManager->setCount() < 1) { delete d->mPersistentSetManager; d->mPersistentSetManager = nullptr; } } #include "moc_model.cpp" diff --git a/messagelist/src/core/view.cpp b/messagelist/src/core/view.cpp index 37323bcd..d0883d6a 100644 --- a/messagelist/src/core/view.cpp +++ b/messagelist/src/core/view.cpp @@ -1,2755 +1,2755 @@ /****************************************************************************** * * Copyright 2008 Szymon Tomasz Stefanek * * 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. * *******************************************************************************/ #include "core/view.h" #include "core/aggregation.h" #include "core/delegate.h" #include "core/groupheaderitem.h" #include "core/item.h" #include "core/manager.h" #include "core/messageitem.h" #include "core/model.h" #include "core/theme.h" #include "messagelistsettings.h" #include "core/storagemodelbase.h" #include "core/widgetbase.h" #include "messagelistutil.h" #include "messagelistutil_p.h" #include "MessageCore/StringUtil" #include // kdepimlibs #include #include #include #include #include #include #include #include #include #include #include #include #include "messagelist_debug.h" using namespace MessageList::Core; class Q_DECL_HIDDEN View::Private { public: Private(View *owner, Widget *parent) : q(owner) , mWidget(parent) , mDelegate(new Delegate(owner)) { } void expandFullThread(const QModelIndex &index); void generalPaletteChanged(); QColor mTextColor; View *const q; Widget *mWidget = nullptr; Model *mModel = nullptr; Delegate *mDelegate = nullptr; const Aggregation *mAggregation = nullptr; ///< The Aggregation we're using now, shallow pointer Theme *mTheme = nullptr; ///< The Theme we're using now, shallow pointer bool mNeedToApplyThemeColumns = false; ///< Flag signaling a pending application of theme columns Item *mLastCurrentItem = nullptr; QPoint mMousePressPosition; bool mSaveThemeColumnStateOnSectionResize = true; ///< This is used to filter out programmatic column resizes in slotSectionResized(). QTimer *mSaveThemeColumnStateTimer = nullptr; ///< Used to trigger a delayed "save theme state" QTimer *mApplyThemeColumnsTimer = nullptr; ///< Used to trigger a delayed "apply theme columns" int mLastViewportWidth = -1; bool mIgnoreUpdateGeometries = false; ///< Shall we ignore the "update geometries" calls ? }; View::View(Widget *pParent) : QTreeView(pParent) , d(new Private(this, pParent)) { d->mSaveThemeColumnStateTimer = new QTimer(); connect(d->mSaveThemeColumnStateTimer, &QTimer::timeout, this, &View::saveThemeColumnState); d->mApplyThemeColumnsTimer = new QTimer(); connect(d->mApplyThemeColumnsTimer, &QTimer::timeout, this, &View::applyThemeColumns); setItemDelegate(d->mDelegate); setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); setAlternatingRowColors(true); setAllColumnsShowFocus(true); setSelectionMode(QAbstractItemView::ExtendedSelection); viewport()->setAcceptDrops(true); header()->setContextMenuPolicy(Qt::CustomContextMenu); connect(header(), &QWidget::customContextMenuRequested, this, &View::slotHeaderContextMenuRequested); connect(header(), &QHeaderView::sectionResized, this, &View::slotHeaderSectionResized); header()->setSectionsClickable(true); header()->setSectionResizeMode(QHeaderView::Interactive); header()->setMinimumSectionSize(2); // QTreeView overrides our sections sizes if we set them smaller than this value header()->setDefaultSectionSize(2); // QTreeView overrides our sections sizes if we set them smaller than this value d->mModel = new Model(this); setModel(d->mModel); connect(d->mModel, &Model::statusMessage, pParent, &Widget::statusMessage); connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &View::slotSelectionChanged, Qt::UniqueConnection); // as in KDE3, when a root-item of a message thread is expanded, expand all children connect(this, &View::expanded, this, [this](const QModelIndex &index) { d->expandFullThread(index); }); } View::~View() { if (d->mSaveThemeColumnStateTimer->isActive()) { d->mSaveThemeColumnStateTimer->stop(); } delete d->mSaveThemeColumnStateTimer; if (d->mApplyThemeColumnsTimer->isActive()) { d->mApplyThemeColumnsTimer->stop(); } delete d->mApplyThemeColumnsTimer; // Zero out the theme, aggregation and ApplyThemeColumnsTimer so Model will not cause accesses to them in its destruction process d->mApplyThemeColumnsTimer = nullptr; d->mTheme = nullptr; d->mAggregation = nullptr; delete d; d = nullptr; } Model *View::model() const { return d->mModel; } Delegate *View::delegate() const { return d->mDelegate; } void View::ignoreCurrentChanges(bool ignore) { if (ignore) { disconnect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &View::slotSelectionChanged); viewport()->setUpdatesEnabled(false); } else { connect(selectionModel(), &QItemSelectionModel::selectionChanged, this, &View::slotSelectionChanged, Qt::UniqueConnection); viewport()->setUpdatesEnabled(true); } } void View::ignoreUpdateGeometries(bool ignore) { d->mIgnoreUpdateGeometries = ignore; } bool View::isScrollingLocked() const { // There is another popular requisite: people want the view to automatically // scroll in order to show new arriving mail. This actually makes sense // only when the view is sorted by date and the new mail is (usually) either // appended at the bottom or inserted at the top. It would be also confusing // when the user is browsing some other thread in the meantime. // // So here we make a simple guess: if the view is scrolled somewhere in the // middle then we assume that the user is browsing other threads and we // try to keep the currently selected item steady on the screen. // When the view is "locked" to the top (scrollbar value 0) or to the // bottom (scrollbar value == maximum) then we assume that the user // isn't browsing and we should attempt to show the incoming messages // by keeping the view "locked". // // The "locking" also doesn't make sense in the first big fill view job. // [Well this concept is pre-akonadi. Now the loading is all async anyway... // So all this code is actually triggered during the initial loading, too.] const int scrollBarPosition = verticalScrollBar()->value(); const int scrollBarMaximum = verticalScrollBar()->maximum(); const SortOrder *sortOrder = d->mModel->sortOrder(); const bool lockView = ( // not the first loading job !d->mModel->isLoading() ) && ( // messages sorted by date (sortOrder->messageSorting() == SortOrder::SortMessagesByDateTime) || (sortOrder->messageSorting() == SortOrder::SortMessagesByDateTimeOfMostRecent) ) && ( // scrollbar at top (Descending order) or bottom (Ascending order) (scrollBarPosition == 0 && sortOrder->messageSortDirection() == SortOrder::Descending) || (scrollBarPosition == scrollBarMaximum && sortOrder->messageSortDirection() == SortOrder::Ascending) ); return lockView; } void View::updateGeometries() { if (d->mIgnoreUpdateGeometries || !d->mModel) { return; } const int scrollBarPositionBefore = verticalScrollBar()->value(); const bool lockView = isScrollingLocked(); QTreeView::updateGeometries(); if (lockView) { // we prefer to keep the view locked to the top or bottom if (scrollBarPositionBefore != 0) { // we wanted the view to be locked to the bottom if (verticalScrollBar()->value() != verticalScrollBar()->maximum()) { verticalScrollBar()->setValue(verticalScrollBar()->maximum()); } } // else we wanted the view to be locked to top and we shouldn't need to do anything } } StorageModel *View::storageModel() const { return d->mModel->storageModel(); } void View::setAggregation(const Aggregation *aggregation) { d->mAggregation = aggregation; d->mModel->setAggregation(aggregation); // use uniform row heights to speed up, but only if there are no group headers used setUniformRowHeights(d->mAggregation->grouping() == Aggregation::NoGrouping); } void View::setTheme(Theme *theme) { d->mNeedToApplyThemeColumns = true; d->mTheme = theme; d->mDelegate->setTheme(theme); d->mModel->setTheme(theme); } void View::setSortOrder(const SortOrder *sortOrder) { d->mModel->setSortOrder(sortOrder); } void View::reload() { setStorageModel(storageModel()); } void View::setStorageModel(StorageModel *storageModel, PreSelectionMode preSelectionMode) { // This will cause the model to be reset. d->mSaveThemeColumnStateOnSectionResize = false; d->mModel->setStorageModel(storageModel, preSelectionMode); d->mSaveThemeColumnStateOnSectionResize = true; } ////////////////////////////////////////////////////////////////////////////////////////////////////// // Theme column state machinery // // This is yet another beast to beat. The QHeaderView behaviour, at the time of writing, // is quite unpredictable. This is due to the complex interaction with the model, with the QTreeView // and due to its attempts to delay the layout jobs. The delayed layouts, especially, may // cause the widths of the columns to quickly change in an unexpected manner in a place // where previously they have been always settled to the values you set... // // So here we have the tools to: // // - Apply the saved state of the theme columns (applyThemeColumns()). // This function computes the "best fit" state of the visible columns and tries // to apply it to QHeaderView. It also saves the new computed state to the Theme object. // // - Explicitly save the column state, used when the user changes the widths or visibility manually. // This is called through a delayed timer after a column has been resized or used directly // when the visibility state of a column has been changed by toggling a popup menu entry. // // - Display the column state context popup menu and handle its actions // // - Apply the theme columns when the theme changes, when the model changes or when // the widget is resized. // // - Avoid saving a corrupted column state in that QHeaderView can be found *very* frequently. // void View::applyThemeColumns() { if (!d->mApplyThemeColumnsTimer) { return; } if (d->mApplyThemeColumnsTimer->isActive()) { d->mApplyThemeColumnsTimer->stop(); } if (!d->mTheme) { return; } //qCDebug(MESSAGELIST_LOG) << "Apply theme columns"; const QList< Theme::Column * > &columns = d->mTheme->columns(); if (columns.isEmpty()) { return; // bad theme } if (!viewport()->isVisible()) { return; // invisible } if (viewport()->width() < 1) { return; // insane width } const int viewportWidth = viewport()->width(); d->mLastViewportWidth = viewportWidth; // Now we want to distribute the available width on all the visible columns. // // The rules: // - The visible columns will span the width of the view, if possible. // - The columns with a saved width should take that width. // - The columns on the left should take more space, if possible. // - The columns with no text take just slightly more than their size hint. // while the columns with text take possibly a lot more. // // Note that the first column is always shown (it can't be hidden at all) // The algorithm below is a sort of compromise between: // - Saving the user preferences for widths // - Using exactly the available view space // // It "tends to work" in all cases: // - When there are no user preferences saved and the column widths must be // automatically computed to make best use of available space // - When there are user preferences for only some of the columns // and that should be somewhat preserved while still using all the // available space. // - When all the columns have well defined saved widths int idx = 0; // Gather total size "hint" for visible sections: if the widths of the columns wers // all saved then the total hint is equal to the total saved width. int totalVisibleWidthHint = 0; QVector< int > lColumnSizeHints; for (const auto col : qAsConst(columns)) { if (col->currentlyVisible() || (idx == 0)) { //qCDebug(MESSAGELIST_LOG) << "Column " << idx << " will be visible"; // Column visible const int savedWidth = col->currentWidth(); const int hintWidth = d->mDelegate->sizeHintForItemTypeAndColumn(Item::Message, idx).width(); totalVisibleWidthHint += savedWidth > 0 ? savedWidth : hintWidth; lColumnSizeHints.append(hintWidth); //qCDebug(MESSAGELIST_LOG) << "Column " << idx << " size hint is " << hintWidth; } else { //qCDebug(MESSAGELIST_LOG) << "Column " << idx << " will be not visible"; // The column is not visible lColumnSizeHints.append(-1); // dummy } idx++; } if (totalVisibleWidthHint < 16) { totalVisibleWidthHint = 16; // be reasonable } // Now compute somewhat "proportional" widths. idx = 0; QVector< double > lColumnWidths; lColumnWidths.reserve(columns.count()); int totalVisibleWidth = 0; for (const auto col : qAsConst(columns)) { double savedWidth = col->currentWidth(); double hintWidth = savedWidth > 0 ? savedWidth : lColumnSizeHints.at(idx); double realWidth; if (col->currentlyVisible() || (idx == 0)) { if (col->containsTextItems()) { // the column contains text items, it should get more space (if possible) realWidth = ((hintWidth * viewportWidth) / totalVisibleWidthHint); } else { // the column contains no text items, it should get exactly its hint/saved width. realWidth = hintWidth; } if (realWidth < 2) { realWidth = 2; // don't allow very insane values } totalVisibleWidth += realWidth; } else { // Column not visible realWidth = -1; } lColumnWidths.append(realWidth); idx++; } // Now the algorithm above may be wrong for several reasons... // - We're using fixed widths for certain columns and proportional // for others... // - The user might have changed the width of the view from the // time in that the widths have been saved // - There are some (not well identified) issues with the QTreeView // scrollbar that make our view appear larger or shorter by 2-3 pixels // sometimes. // - ... // So we correct the previous estimates by trying to use exactly // the available space. idx = 0; if (totalVisibleWidth != viewportWidth) { // The estimated widths were not using exactly the available space. if (totalVisibleWidth < viewportWidth) { // We were using less space than available. // Give the additional space to the text columns // also give more space to the first ones and less space to the last ones qreal available = viewportWidth - totalVisibleWidth; for (int idx = 0; idx < columns.count(); ++idx) { Theme::Column *column = columns.at(idx); if ((column->currentlyVisible() || (idx == 0)) && column->containsTextItems()) { // give more space to this column available /= 2; // eat half of the available space lColumnWidths[ idx ] += available; // and give it to this column if (available < 1) { break; // no more space to give away } } } // if any space is still available, give it to the first column if (available >= 1) { lColumnWidths[ 0 ] += available; } } else { // We were using more space than available // If the columns span more than the view then // try to squeeze them in order to make them fit double missing = totalVisibleWidth - viewportWidth; if (missing > 0) { const int count = lColumnWidths.count(); idx = count - 1; while (idx >= 0) { if (columns.at(idx)->currentlyVisible() || (idx == 0)) { double chop = lColumnWidths.at(idx) - lColumnSizeHints.at(idx); if (chop > 0) { if (chop > missing) { chop = missing; } lColumnWidths[ idx ] -= chop; missing -= chop; if (missing < 1) { break; // no more space to recover } } } // else it's invisible idx--; } } } } // We're ready to assign widths. bool oldSave = d->mSaveThemeColumnStateOnSectionResize; d->mSaveThemeColumnStateOnSectionResize = false; // A huge problem here is that QHeaderView goes quite nuts if we show or hide sections // while resizing them. This is because it has several machineries aimed to delay // the layout to the last possible moment. So if we show a column, it will tend to // screw up the layout of other ones. // We first loop showing/hiding columns then. idx = 0; for (const auto col : qAsConst(columns)) { bool visible = (idx == 0) || col->currentlyVisible(); //qCDebug(MESSAGELIST_LOG) << "Column " << idx << " visible " << visible; col->setCurrentlyVisible(visible); header()->setSectionHidden(idx, !visible); idx++; } // Then we loop assigning widths. This is still complicated since QHeaderView tries // very badly to stretch the last section and thus will resize it in the meantime. // But seems to work most of the times... idx = 0; for (const auto col : qAsConst(columns)) { if (col->currentlyVisible()) { const double columnWidth(lColumnWidths.at(idx)); col->setCurrentWidth(columnWidth); //Laurent Bug 358855 - message list column widths lost when program closed // I need to investigate if this code is still necessary (all method) header()->resizeSection(idx, static_cast(columnWidth)); } else { col->setCurrentWidth(-1); } idx++; } idx = 0; bool bTriggeredQtBug = false; for (const auto col : qAsConst(columns)) { if (!header()->isSectionHidden(idx)) { if (!col->currentlyVisible()) { bTriggeredQtBug = true; } } idx++; } setHeaderHidden(d->mTheme->viewHeaderPolicy() == Theme::NeverShowHeader); d->mSaveThemeColumnStateOnSectionResize = oldSave; d->mNeedToApplyThemeColumns = false; static bool bAllowRecursion = true; if (bTriggeredQtBug && bAllowRecursion) { bAllowRecursion = false; //qCDebug(MESSAGELIST_LOG) << "I've triggered the QHeaderView bug: trying to fix by calling myself again"; applyThemeColumns(); bAllowRecursion = true; } } void View::triggerDelayedApplyThemeColumns() { if (d->mApplyThemeColumnsTimer->isActive()) { d->mApplyThemeColumnsTimer->stop(); } d->mApplyThemeColumnsTimer->setSingleShot(true); d->mApplyThemeColumnsTimer->start(100); } void View::saveThemeColumnState() { if (d->mSaveThemeColumnStateTimer->isActive()) { d->mSaveThemeColumnStateTimer->stop(); } if (!d->mTheme) { return; } if (d->mNeedToApplyThemeColumns) { return; // don't save the state if it hasn't been applied at all } //qCDebug(MESSAGELIST_LOG) << "Save theme column state"; const auto columns = d->mTheme->columns(); if (columns.isEmpty()) { return; // bad theme } int idx = 0; for (const auto col : qAsConst(columns)) { if (header()->isSectionHidden(idx)) { //qCDebug(MESSAGELIST_LOG) << "Section " << idx << " is hidden"; col->setCurrentlyVisible(false); col->setCurrentWidth(-1); // reset (hmmm... we could use the "don't touch" policy here too...) } else { //qCDebug(MESSAGELIST_LOG) << "Section " << idx << " is visible and has size " << header()->sectionSize( idx ); col->setCurrentlyVisible(true); col->setCurrentWidth(header()->sectionSize(idx)); } idx++; } } void View::triggerDelayedSaveThemeColumnState() { if (d->mSaveThemeColumnStateTimer->isActive()) { d->mSaveThemeColumnStateTimer->stop(); } d->mSaveThemeColumnStateTimer->setSingleShot(true); d->mSaveThemeColumnStateTimer->start(200); } void View::resizeEvent(QResizeEvent *e) { qCDebug(MESSAGELIST_LOG) << "Resize event enter (viewport width is " << viewport()->width() << ")"; QTreeView::resizeEvent(e); if (!isVisible()) { return; // don't play with } if (d->mLastViewportWidth != viewport()->width()) { triggerDelayedApplyThemeColumns(); } if (header()->isVisible()) { return; } // header invisible bool oldSave = d->mSaveThemeColumnStateOnSectionResize; d->mSaveThemeColumnStateOnSectionResize = false; const int count = header()->count(); if ((count - header()->hiddenSectionCount()) < 2) { // a single column visible: resize it int visibleIndex; for (visibleIndex = 0; visibleIndex < count; visibleIndex++) { if (!header()->isSectionHidden(visibleIndex)) { break; } } if (visibleIndex < count) { header()->resizeSection(visibleIndex, viewport()->width() - 4); } } d->mSaveThemeColumnStateOnSectionResize = oldSave; triggerDelayedSaveThemeColumnState(); } void View::paintEvent(QPaintEvent *event) { #if 0 if (/*mFirstResult &&*/ (!model() || model()->rowCount() == 0)) { QPainter p(viewport()); QFont font = p.font(); font.setItalic(true); p.setFont(font); if (!d->mTextColor.isValid()) { d->generalPaletteChanged(); } p.setPen(d->mTextColor); p.drawText(QRect(0, 0, width(), height()), Qt::AlignCenter, i18n("No result found")); } else { QTreeView::paintEvent(event); } #else QTreeView::paintEvent(event); #endif } void View::modelAboutToEmitLayoutChanged() { // QHeaderView goes totally NUTS with a layoutChanged() call d->mSaveThemeColumnStateOnSectionResize = false; } void View::modelEmittedLayoutChanged() { // This is after a first chunk of work has been done by the model: do apply column states d->mSaveThemeColumnStateOnSectionResize = true; applyThemeColumns(); } void View::slotHeaderSectionResized(int logicalIndex, int oldWidth, int newWidth) { Q_UNUSED(logicalIndex); Q_UNUSED(oldWidth); Q_UNUSED(newWidth); if (d->mSaveThemeColumnStateOnSectionResize) { triggerDelayedSaveThemeColumnState(); } } int View::sizeHintForColumn(int logicalColumnIndex) const { // QTreeView: please don't touch my column widths... int w = header()->sectionSize(logicalColumnIndex); if (w > 0) { return w; } if (!d->mDelegate) { return 32; // dummy } w = d->mDelegate->sizeHintForItemTypeAndColumn(Item::Message, logicalColumnIndex).width(); return w; } void View::showEvent(QShowEvent *e) { QTreeView::showEvent(e); } void View::slotHeaderContextMenuRequested(const QPoint &pnt) { if (!d->mTheme) { return; } const auto columns = d->mTheme->columns(); if (columns.isEmpty()) { return; // bad theme } // the menu for the columns QMenu menu; int idx = 0; for (const auto col : qAsConst(columns)) { QAction *act = menu.addAction(col->label()); act->setCheckable(true); act->setChecked(!header()->isSectionHidden(idx)); if (idx == 0) { act->setEnabled(false); } QObject::connect(act, &QAction::triggered, this, [this, idx] { slotShowHideColumn(idx); }); idx++; } menu.addSeparator(); { QAction *act = menu.addAction(i18n("Adjust Column Sizes")); QObject::connect(act, &QAction::triggered, this, &View::slotAdjustColumnSizes); } { QAction *act = menu.addAction(i18n("Show Default Columns")); QObject::connect(act, &QAction::triggered, this, &View::slotShowDefaultColumns); } menu.addSeparator(); { QAction *act = menu.addAction(i18n("Display Tooltips")); act->setCheckable(true); act->setChecked(MessageListSettings::self()->messageToolTipEnabled()); QObject::connect(act, &QAction::triggered, this, &View::slotDisplayTooltips); } menu.addSeparator(); MessageList::Util::fillViewMenu(&menu, d->mWidget); menu.exec(header()->mapToGlobal(pnt)); } void View::slotAdjustColumnSizes() { if (!d->mTheme) { return; } d->mTheme->resetColumnSizes(); applyThemeColumns(); } void View::slotShowDefaultColumns() { if (!d->mTheme) { return; } d->mTheme->resetColumnState(); applyThemeColumns(); } void View::slotDisplayTooltips(bool showTooltips) { MessageListSettings::self()->setMessageToolTipEnabled(showTooltips); } void View::slotShowHideColumn(int columnIdx) { if (!d->mTheme) { return; // oops } if (columnIdx == 0) { return; // can never be hidden } if (columnIdx >= d->mTheme->columns().count()) { return; } const bool showIt = header()->isSectionHidden(columnIdx); Theme::Column *column = d->mTheme->columns().at(columnIdx); Q_ASSERT(column); // first save column state (as it is, with the column still in previous state) saveThemeColumnState(); // If a section has just been shown, invalidate its width in the skin // since QTreeView assigned it a (possibly insane) default width. // If a section has been hidden, then invalidate its width anyway... // so finally invalidate width always, here. column->setCurrentlyVisible(showIt); column->setCurrentWidth(-1); // then apply theme columns to re-compute proportional widths (so we hopefully stay in the view) applyThemeColumns(); } Item *View::currentItem() const { QModelIndex idx = currentIndex(); if (!idx.isValid()) { return nullptr; } Item *it = static_cast< Item * >(idx.internalPointer()); Q_ASSERT(it); return it; } MessageItem *View::currentMessageItem(bool selectIfNeeded) const { Item *it = currentItem(); if (!it || (it->type() != Item::Message)) { return nullptr; } if (selectIfNeeded) { // Keep things coherent, if the user didn't select it, but acted on it via // a shortcut, do select it now. if (!selectionModel()->isSelected(currentIndex())) { selectionModel()->select(currentIndex(), QItemSelectionModel::Select | QItemSelectionModel::Current | QItemSelectionModel::Rows); } } return static_cast< MessageItem * >(it); } void View::setCurrentMessageItem(MessageItem *it, bool center) { if (it) { qCDebug(MESSAGELIST_LOG) << "Setting current message to" << it->subject(); const QModelIndex index = d->mModel->index(it, 0); selectionModel()->setCurrentIndex(index, QItemSelectionModel::Select |QItemSelectionModel::Current | QItemSelectionModel::Rows); if (center) { scrollTo(index, QAbstractItemView::PositionAtCenter); } } else { selectionModel()->setCurrentIndex(QModelIndex(), QItemSelectionModel::Current |QItemSelectionModel::Clear); } } bool View::selectionEmpty() const { return selectionModel()->selectedRows().isEmpty(); } QVector< MessageItem * > View::selectionAsMessageItemList(bool includeCollapsedChildren) const { QVector< MessageItem * > selectedMessages; QModelIndexList lSelected = selectionModel()->selectedRows(); if (lSelected.isEmpty()) { return selectedMessages; } for (const auto &idx : qAsConst(lSelected)) { // The asserts below are theoretically valid but at the time // of writing they fail because of a bug in QItemSelectionModel::selectedRows() // which returns also non-selectable items. //Q_ASSERT( selectedItem->type() == Item::Message ); //Q_ASSERT( ( *it ).isValid() ); if (!idx.isValid()) { continue; } Item *selectedItem = static_cast(idx.internalPointer()); Q_ASSERT(selectedItem); if (selectedItem->type() != Item::Message) { continue; } if (!static_cast(selectedItem)->isValid()) { continue; } Q_ASSERT(!selectedMessages.contains(static_cast(selectedItem))); if (includeCollapsedChildren && (selectedItem->childItemCount() > 0) && (!isExpanded(idx))) { static_cast(selectedItem)->subTreeToList(selectedMessages); } else { selectedMessages.append(static_cast(selectedItem)); } } return selectedMessages; } QVector View::currentThreadAsMessageItemList() const { QVector currentThread; MessageItem *msg = currentMessageItem(); if (!msg) { return currentThread; } while (msg->parent()) { if (msg->parent()->type() != Item::Message) { break; } msg = static_cast< MessageItem * >(msg->parent()); } msg->subTreeToList(currentThread); return currentThread; } void View::setChildrenExpanded(const Item *root, bool expand) { Q_ASSERT(root); auto childList = root->childItems(); if (!childList) { return; } for (const auto child : qAsConst(*childList)) { QModelIndex idx = d->mModel->index(child, 0); Q_ASSERT(idx.isValid()); Q_ASSERT(static_cast(idx.internalPointer()) == child); if (expand) { setExpanded(idx, true); if (child->childItemCount() > 0) { setChildrenExpanded(child, true); } } else { if (child->childItemCount() > 0) { setChildrenExpanded(child, false); } setExpanded(idx, false); } } } void View::Private::generalPaletteChanged() { const QPalette palette = q->viewport()->palette(); QColor color = palette.text().color(); color.setAlpha(128); mTextColor = color; } void View::Private::expandFullThread(const QModelIndex &index) { if (!index.isValid()) { return; } Item *item = static_cast< Item * >(index.internalPointer()); if (item->type() != Item::Message) { return; } if (!static_cast< MessageItem * >(item)->parent() || (static_cast< MessageItem * >(item)->parent()->type() != Item::Message)) { q->setChildrenExpanded(item, true); } } void View::setCurrentThreadExpanded(bool expand) { Item *it = currentItem(); if (!it) { return; } if (it->type() == Item::GroupHeader) { setExpanded(currentIndex(), expand); } else if (it->type() == Item::Message) { MessageItem *message = static_cast< MessageItem *>(it); while (message->parent()) { if (message->parent()->type() != Item::Message) { break; } message = static_cast< MessageItem * >(message->parent()); } if (expand) { setExpanded(d->mModel->index(message, 0), true); setChildrenExpanded(message, true); } else { setChildrenExpanded(message, false); setExpanded(d->mModel->index(message, 0), false); } } } void View::setAllThreadsExpanded(bool expand) { scheduleDelayedItemsLayout(); if (d->mAggregation->grouping() == Aggregation::NoGrouping) { // we have no groups so threads start under the root item: just expand/unexpand all setChildrenExpanded(d->mModel->rootItem(), expand); return; } // grouping is in effect: must expand/unexpand one level lower auto childList = d->mModel->rootItem()->childItems(); if (!childList) { return; } for (const auto item : qAsConst(*childList)) { setChildrenExpanded(item, expand); } } void View::setAllGroupsExpanded(bool expand) { if (d->mAggregation->grouping() == Aggregation::NoGrouping) { return; // no grouping in effect } Item *item = d->mModel->rootItem(); auto childList = item->childItems(); if (!childList) { return; } scheduleDelayedItemsLayout(); for (const auto item : qAsConst(*childList)) { Q_ASSERT(item->type() == Item::GroupHeader); QModelIndex idx = d->mModel->index(item, 0); Q_ASSERT(idx.isValid()); Q_ASSERT(static_cast< Item * >(idx.internalPointer()) == item); if (expand) { if (!isExpanded(idx)) { setExpanded(idx, true); } } else { if (isExpanded(idx)) { setExpanded(idx, false); } } } } void View::selectMessageItems(const QVector< MessageItem * > &list) { QItemSelection selection; for (const auto mi : list) { Q_ASSERT(mi); QModelIndex idx = d->mModel->index(mi, 0); Q_ASSERT(idx.isValid()); Q_ASSERT(static_cast(idx.internalPointer()) == mi); if (!selectionModel()->isSelected(idx)) { selection.append(QItemSelectionRange(idx)); } ensureDisplayedWithParentsExpanded(mi); } if (!selection.isEmpty()) { selectionModel()->select(selection, QItemSelectionModel::Select | QItemSelectionModel::Rows); } } static inline bool message_type_matches(Item *item, MessageTypeFilter messageTypeFilter) { switch (messageTypeFilter) { case MessageTypeAny: return true; break; case MessageTypeUnreadOnly: return !item->status().isRead(); break; default: // nothing here break; } // never reached Q_ASSERT(false); return false; } Item *View::messageItemAfter(Item *referenceItem, MessageTypeFilter messageTypeFilter, bool loop) { if (!storageModel()) { return nullptr; // no folder } // find the item to start with Item *below; if (referenceItem) { // there was a current item: we start just below it if ( (referenceItem->childItemCount() > 0) && ( (messageTypeFilter != MessageTypeAny) || isExpanded(d->mModel->index(referenceItem, 0)) ) ) { // the current item had children: either expanded or we want unread/new messages (and so we'll expand it if it isn't) below = referenceItem->itemBelow(); } else { // the current item had no children: ask the parent to find the item below Q_ASSERT(referenceItem->parent()); below = referenceItem->parent()->itemBelowChild(referenceItem); } if (!below) { // reached the end if (loop) { // try re-starting from top below = d->mModel->rootItem()->itemBelow(); Q_ASSERT(below); // must exist (we had a current item) if (below == referenceItem) { return nullptr; // only one item in folder: loop complete } } else { // looping not requested return nullptr; } } } else { // there was no current item, start from beginning below = d->mModel->rootItem()->itemBelow(); if (!below) { return nullptr; // folder empty } } // ok.. now below points to the next message. // While it doesn't satisfy our requirements, go further down QModelIndex parentIndex = d->mModel->index(below->parent(), 0); QModelIndex belowIndex = d->mModel->index(below, 0); Q_ASSERT(belowIndex.isValid()); while ( // is not a message (we want messages, don't we ?) (below->type() != Item::Message) ||// message filter doesn't match (!message_type_matches(below, messageTypeFilter)) ||// is hidden (and we don't want hidden items as they arent "officially" in the view) isRowHidden(belowIndex.row(), parentIndex) ||// is not enabled or not selectable ((d->mModel->flags(belowIndex) & (Qt::ItemIsSelectable | Qt::ItemIsEnabled)) != (Qt::ItemIsSelectable | Qt::ItemIsEnabled)) ) { // find the next one if ((below->childItemCount() > 0) && ((messageTypeFilter != MessageTypeAny) || isExpanded(belowIndex))) { // the current item had children: either expanded or we want unread messages (and so we'll expand it if it isn't) below = below->itemBelow(); } else { // the current item had no children: ask the parent to find the item below Q_ASSERT(below->parent()); below = below->parent()->itemBelowChild(below); } if (!below) { // we reached the end of the folder if (loop) { // looping requested if (referenceItem) { // <-- this means "we have started from something that is not the top: looping makes sense" below = d->mModel->rootItem()->itemBelow(); } // else mi == 0 and below == 0: we have started from the beginning and reached the end (it will fail the test below and exit) } else { // looping not requested: nothing more to do return nullptr; } } if (below == referenceItem) { Q_ASSERT(loop); return nullptr; // looped and returned back to the first message } parentIndex = d->mModel->index(below->parent(), 0); belowIndex = d->mModel->index(below, 0); Q_ASSERT(belowIndex.isValid()); } return below; } Item *View::firstMessageItem(MessageTypeFilter messageTypeFilter) { return messageItemAfter(nullptr, messageTypeFilter, false); } Item *View::nextMessageItem(MessageTypeFilter messageTypeFilter, bool loop) { return messageItemAfter(currentMessageItem(false), messageTypeFilter, loop); } Item *View::deepestExpandedChild(Item *referenceItem) const { const int children = referenceItem->childItemCount(); if (children > 0 && isExpanded(d->mModel->index(referenceItem, 0))) { return deepestExpandedChild(referenceItem->childItem(children - 1)); } else { return referenceItem; } } Item *View::messageItemBefore(Item *referenceItem, MessageTypeFilter messageTypeFilter, bool loop) { if (!storageModel()) { return nullptr; // no folder } // find the item to start with Item *above; if (referenceItem) { Item *parent = referenceItem->parent(); Item *siblingAbove = parent ? parent->itemAboveChild(referenceItem) : nullptr; // there was a current item: we start just above it if ((siblingAbove && siblingAbove != referenceItem && siblingAbove != parent) && (siblingAbove->childItemCount() > 0) && ( (messageTypeFilter != MessageTypeAny) || (isExpanded(d->mModel->index(siblingAbove, 0))) ) ) { // the current item had children: either expanded or we want unread/new messages (and so we'll expand it if it isn't) above = deepestExpandedChild(siblingAbove); } else { // the current item had no children: ask the parent to find the item above Q_ASSERT(referenceItem->parent()); above = referenceItem->parent()->itemAboveChild(referenceItem); } if ((!above) || (above == d->mModel->rootItem())) { // reached the beginning if (loop) { // try re-starting from bottom above = d->mModel->rootItem()->deepestItem(); Q_ASSERT(above); // must exist (we had a current item) Q_ASSERT(above != d->mModel->rootItem()); if (above == referenceItem) { return nullptr; // only one item in folder: loop complete } } else { // looping not requested return nullptr; } } } else { // there was no current item, start from end above = d->mModel->rootItem()->deepestItem(); if (!above || !above->parent() || (above == d->mModel->rootItem())) { return nullptr; // folder empty } } // ok.. now below points to the previous message. // While it doesn't satisfy our requirements, go further up QModelIndex parentIndex = d->mModel->index(above->parent(), 0); QModelIndex aboveIndex = d->mModel->index(above, 0); Q_ASSERT(aboveIndex.isValid()); while ( // is not a message (we want messages, don't we ?) (above->type() != Item::Message) ||// message filter doesn't match (!message_type_matches(above, messageTypeFilter)) ||// we don't expand items but the item has parents unexpanded (so should be skipped) ( // !expand items (messageTypeFilter == MessageTypeAny) &&// has unexpanded parents or is itself hidden (!isDisplayedWithParentsExpanded(above)) ) ||// is hidden isRowHidden(aboveIndex.row(), parentIndex) ||// is not enabled or not selectable ((d->mModel->flags(aboveIndex) & (Qt::ItemIsSelectable | Qt::ItemIsEnabled)) != (Qt::ItemIsSelectable | Qt::ItemIsEnabled)) ) { above = above->itemAbove(); if ((!above) || (above == d->mModel->rootItem())) { // reached the beginning if (loop) { // looping requested if (referenceItem) { // <-- this means "we have started from something that is not the beginning: looping makes sense" above = d->mModel->rootItem()->deepestItem(); } // else mi == 0 and above == 0: we have started from the end and reached the beginning (it will fail the test below and exit) } else { // looping not requested: nothing more to do return nullptr; } } if (above == referenceItem) { Q_ASSERT(loop); return nullptr; // looped and returned back to the first message } if (!above->parent()) { return nullptr; } parentIndex = d->mModel->index(above->parent(), 0); aboveIndex = d->mModel->index(above, 0); Q_ASSERT(aboveIndex.isValid()); } return above; } Item *View::lastMessageItem(MessageTypeFilter messageTypeFilter) { return messageItemBefore(nullptr, messageTypeFilter, false); } Item *View::previousMessageItem(MessageTypeFilter messageTypeFilter, bool loop) { return messageItemBefore(currentMessageItem(false), messageTypeFilter, loop); } void View::growOrShrinkExistingSelection(const QModelIndex &newSelectedIndex, bool movingUp) { // Qt: why visualIndex() is private? ...I'd really need it here... int selectedVisualCoordinate = visualRect(newSelectedIndex).top(); int topVisualCoordinate = 0xfffffff; // huuuuuge number int bottomVisualCoordinate = -(0xfffffff); int candidate; QModelIndex bottomIndex; QModelIndex topIndex; // find out the actual selection range const QItemSelection selection = selectionModel()->selection(); for (const QItemSelectionRange &range : selection) { // We're asking the model for the index as range.topLeft() and range.bottomRight() // can return indexes in invisible columns which have a null visualRect(). // Column 0, instead, is always visible. QModelIndex top = d->mModel->index(range.top(), 0, range.parent()); QModelIndex bottom = d->mModel->index(range.bottom(), 0, range.parent()); if (top.isValid()) { if (!bottom.isValid()) { bottom = top; } } else { if (!top.isValid()) { top = bottom; } } candidate = visualRect(bottom).bottom(); if (candidate > bottomVisualCoordinate) { bottomVisualCoordinate = candidate; bottomIndex = range.bottomRight(); } candidate = visualRect(top).top(); if (candidate < topVisualCoordinate) { topVisualCoordinate = candidate; topIndex = range.topLeft(); } } if (topIndex.isValid() && bottomIndex.isValid()) { if (movingUp) { if (selectedVisualCoordinate < topVisualCoordinate) { // selecting something above the top: grow selection selectionModel()->select(newSelectedIndex, QItemSelectionModel::Rows | QItemSelectionModel::Select); } else { // selecting something below the top: shrink selection const QModelIndexList selectedIndexes = selection.indexes(); for (const QModelIndex &idx : selectedIndexes) { if ((idx.column() == 0) && (visualRect(idx).top() > selectedVisualCoordinate)) { selectionModel()->select(idx, QItemSelectionModel::Rows | QItemSelectionModel::Deselect); } } } } else { if (selectedVisualCoordinate > bottomVisualCoordinate) { // selecting something below bottom: grow selection selectionModel()->select(newSelectedIndex, QItemSelectionModel::Rows | QItemSelectionModel::Select); } else { // selecting something above bottom: shrink selection const QModelIndexList selectedIndexes = selection.indexes(); for (const QModelIndex &idx : selectedIndexes) { if ((idx.column() == 0) && (visualRect(idx).top() < selectedVisualCoordinate)) { selectionModel()->select(idx, QItemSelectionModel::Rows | QItemSelectionModel::Deselect); } } } } } else { // no existing selection, just grow selectionModel()->select(newSelectedIndex, QItemSelectionModel::Rows | QItemSelectionModel::Select); } } bool View::selectNextMessageItem( MessageTypeFilter messageTypeFilter, ExistingSelectionBehaviour existingSelectionBehaviour, bool centerItem, bool loop) { Item *it = nextMessageItem(messageTypeFilter, loop); if (!it) { return false; } if (it->parent() != d->mModel->rootItem()) { ensureDisplayedWithParentsExpanded(it); } QModelIndex idx = d->mModel->index(it, 0); Q_ASSERT(idx.isValid()); switch (existingSelectionBehaviour) { case ExpandExistingSelection: selectionModel()->setCurrentIndex(idx, QItemSelectionModel::NoUpdate); selectionModel()->select(idx, QItemSelectionModel::Rows | QItemSelectionModel::Select); break; case GrowOrShrinkExistingSelection: selectionModel()->setCurrentIndex(idx, QItemSelectionModel::NoUpdate); growOrShrinkExistingSelection(idx, false); break; default: //case ClearExistingSelection: setCurrentIndex(idx); break; } if (centerItem) { scrollTo(idx, QAbstractItemView::PositionAtCenter); } return true; } bool View::selectPreviousMessageItem( MessageTypeFilter messageTypeFilter, ExistingSelectionBehaviour existingSelectionBehaviour, bool centerItem, bool loop) { Item *it = previousMessageItem(messageTypeFilter, loop); if (!it) { return false; } if (it->parent() != d->mModel->rootItem()) { ensureDisplayedWithParentsExpanded(it); } QModelIndex idx = d->mModel->index(it, 0); Q_ASSERT(idx.isValid()); switch (existingSelectionBehaviour) { case ExpandExistingSelection: selectionModel()->setCurrentIndex(idx, QItemSelectionModel::NoUpdate); selectionModel()->select(idx, QItemSelectionModel::Rows | QItemSelectionModel::Select); break; case GrowOrShrinkExistingSelection: selectionModel()->setCurrentIndex(idx, QItemSelectionModel::NoUpdate); growOrShrinkExistingSelection(idx, true); break; default: //case ClearExistingSelection: setCurrentIndex(idx); break; } if (centerItem) { scrollTo(idx, QAbstractItemView::PositionAtCenter); } return true; } bool View::focusNextMessageItem(MessageTypeFilter messageTypeFilter, bool centerItem, bool loop) { Item *it = nextMessageItem(messageTypeFilter, loop); if (!it) { return false; } if (it->parent() != d->mModel->rootItem()) { ensureDisplayedWithParentsExpanded(it); } QModelIndex idx = d->mModel->index(it, 0); Q_ASSERT(idx.isValid()); selectionModel()->setCurrentIndex(idx, QItemSelectionModel::NoUpdate); if (centerItem) { scrollTo(idx, QAbstractItemView::PositionAtCenter); } return true; } bool View::focusPreviousMessageItem(MessageTypeFilter messageTypeFilter, bool centerItem, bool loop) { Item *it = previousMessageItem(messageTypeFilter, loop); if (!it) { return false; } if (it->parent() != d->mModel->rootItem()) { ensureDisplayedWithParentsExpanded(it); } QModelIndex idx = d->mModel->index(it, 0); Q_ASSERT(idx.isValid()); selectionModel()->setCurrentIndex(idx, QItemSelectionModel::NoUpdate); if (centerItem) { scrollTo(idx, QAbstractItemView::PositionAtCenter); } return true; } void View::selectFocusedMessageItem(bool centerItem) { QModelIndex idx = currentIndex(); if (!idx.isValid()) { return; } if (selectionModel()->isSelected(idx)) { return; } selectionModel()->select(idx, QItemSelectionModel::Select | QItemSelectionModel::Current | QItemSelectionModel::Rows); if (centerItem) { scrollTo(idx, QAbstractItemView::PositionAtCenter); } } bool View::selectFirstMessageItem(MessageTypeFilter messageTypeFilter, bool centerItem) { if (!storageModel()) { return false; // nothing to do } Item *it = firstMessageItem(messageTypeFilter); if (!it) { return false; } Q_ASSERT(it != d->mModel->rootItem()); // must never happen (obviously) ensureDisplayedWithParentsExpanded(it); QModelIndex idx = d->mModel->index(it, 0); Q_ASSERT(idx.isValid()); setCurrentIndex(idx); if (centerItem) { scrollTo(idx, QAbstractItemView::PositionAtCenter); } return true; } bool View::selectLastMessageItem(MessageTypeFilter messageTypeFilter, bool centerItem) { if (!storageModel()) { return false; } Item *it = lastMessageItem(messageTypeFilter); if (!it) { return false; } Q_ASSERT(it != d->mModel->rootItem()); ensureDisplayedWithParentsExpanded(it); QModelIndex idx = d->mModel->index(it, 0); Q_ASSERT(idx.isValid()); setCurrentIndex(idx); if (centerItem) { scrollTo(idx, QAbstractItemView::PositionAtCenter); } return true; } void View::modelFinishedLoading() { Q_ASSERT(storageModel()); Q_ASSERT(!d->mModel->isLoading()); // nothing here for now :) } MessageItemSetReference View::createPersistentSet(const QVector &items) { return d->mModel->createPersistentSet(items); } QList< MessageItem * > View::persistentSetCurrentMessageItemList(MessageItemSetReference ref) { return d->mModel->persistentSetCurrentMessageItemList(ref); } void View::deletePersistentSet(MessageItemSetReference ref) { d->mModel->deletePersistentSet(ref); } void View::markMessageItemsAsAboutToBeRemoved(const QList &items, bool bMark) { if (!bMark) { for (const auto mi : items) { if (mi->isValid()) { // hasn't been removed in the meantime mi->setAboutToBeRemoved(false); } } viewport()->update(); return; } // ok.. we're going to mark the messages as "about to be deleted". // This means that we're going to make them non selectable. // What happens to the selection is generally an untrackable big mess. // Several components and entities are involved. // Qutie tries to apply some kind of internal logic in order to keep // "something" selected and "something" (else) to be current. // The results sometimes appear to depend on the current moon phase. // The Model will do crazy things in order to preserve the current // selection (and possibly the current item). If it's impossible then // it will make its own guesses about what should be selected next. // A problem is that the Model will do it one message at a time. // When item reparenting/reordering is involved then the guesses // can produce non-intuitive results. // Add the fact that selection and current item are distinct concepts, // their relative interaction depends on the settings and is often quite // unclear. // Add the fact that (at the time of writing) several styles don't show // the current item (only Yoda knows why) and this causes some confusion to the user. // Add the fact that the operations are asynchronous: deletion will start // a job, do some event loop processing and then complete the work at a later time. // The Qutie views also tend to accumulate the changes and perform them // all at once at the latest possible stage. // A radical approach is needed: we FIRST deal with the selection // by tring to move it away from the messages about to be deleted // and THEN mark the (hopefully no longer selected) messages as "about to be deleted". // First of all, find out if we're going to clear the entire selection (very likely). bool clearingEntireSelection = true; const QModelIndexList selectedIndexes = selectionModel()->selectedRows(0); if (selectedIndexes.count() > items.count()) { // the selection is bigger: we can't clear it completely clearingEntireSelection = false; } else { // the selection has same size or is smaller: we can clear it completely with our removal for (const QModelIndex &selectedIndex : selectedIndexes) { Q_ASSERT(selectedIndex.isValid()); Q_ASSERT(selectedIndex.column() == 0); Item *selectedItem = static_cast< Item * >(selectedIndex.internalPointer()); Q_ASSERT(selectedItem); if (selectedItem->type() != Item::Message) { continue; } if (!items.contains(static_cast< MessageItem * >(selectedItem))) { // the selection contains something that we aren't going to remove: // we will not clear the selection completely clearingEntireSelection = false; break; } } } if (clearingEntireSelection) { // Try to clear the current selection and select something sensible instead, // so after the deletion we will not end up with a random selection. // Pick up a message in the set (which is very likely to be contiguous), walk the tree // and select the next message that is NOT in the set. MessageItem *aMessage = items.last(); Q_ASSERT(aMessage); // Avoid infinite loops by carrying only a limited number of attempts. // If there is any message that is not in the set then items.count() attempts should find it. int maxAttempts = items.count(); while (items.contains(aMessage) && (maxAttempts > 0)) { Item *next = messageItemAfter(aMessage, MessageTypeAny, false); if (!next) { // no way aMessage = nullptr; break; } Q_ASSERT(next->type() == Item::Message); aMessage = static_cast< MessageItem * >(next); maxAttempts--; } if (!aMessage) { // try backwards aMessage = items.first(); Q_ASSERT(aMessage); maxAttempts = items.count(); while (items.contains(aMessage) && (maxAttempts > 0)) { Item *prev = messageItemBefore(aMessage, MessageTypeAny, false); if (!prev) { // no way aMessage = nullptr; break; } Q_ASSERT(prev->type() == Item::Message); aMessage = static_cast< MessageItem * >(prev); maxAttempts--; } } if (aMessage) { QModelIndex aMessageIndex = d->mModel->index(aMessage, 0); Q_ASSERT(aMessageIndex.isValid()); Q_ASSERT(static_cast< MessageItem * >(aMessageIndex.internalPointer()) == aMessage); Q_ASSERT(!selectionModel()->isSelected(aMessageIndex)); setCurrentIndex(aMessageIndex); selectionModel()->select(aMessageIndex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); } } // else we aren't clearing the entire selection so something should just stay selected. // Now mark messages as about to be removed. for (const auto mi : items) { mi->setAboutToBeRemoved(true); QModelIndex idx = d->mModel->index(mi, 0); Q_ASSERT(idx.isValid()); Q_ASSERT(static_cast< MessageItem * >(idx.internalPointer()) == mi); if (selectionModel()->isSelected(idx)) { selectionModel()->select(idx, QItemSelectionModel::Deselect | QItemSelectionModel::Rows); } } viewport()->update(); } void View::ensureDisplayedWithParentsExpanded(Item *it) { Q_ASSERT(it); Q_ASSERT(it->parent()); Q_ASSERT(it->isViewable()); // must be attached to the viewable root if (isRowHidden(it->parent()->indexOfChildItem(it), d->mModel->index(it->parent(), 0))) { setRowHidden(it->parent()->indexOfChildItem(it), d->mModel->index(it->parent(), 0), false); } it = it->parent(); while (it->parent()) { if (isRowHidden(it->parent()->indexOfChildItem(it), d->mModel->index(it->parent(), 0))) { setRowHidden(it->parent()->indexOfChildItem(it), d->mModel->index(it->parent(), 0), false); } QModelIndex idx = d->mModel->index(it, 0); Q_ASSERT(idx.isValid()); Q_ASSERT(static_cast< Item * >(idx.internalPointer()) == it); if (!isExpanded(idx)) { setExpanded(idx, true); } it = it->parent(); } } bool View::isDisplayedWithParentsExpanded(Item *it) const { // An item is currently viewable iff // - it is marked as viewable in the item structure (that is, qt knows about its existence) // (and this means that all of its parents are marked as viewable) // - it is not explicitly hidden // - all of its parents are expanded if (!it) { return false; // be nice and allow the caller not to care } if (!it->isViewable()) { return false; // item not viewable (not attached to the viewable root or qt not yet aware of it) } // the item and all the parents are marked as viewable. if (isRowHidden(it->parent()->indexOfChildItem(it), d->mModel->index(it->parent(), 0))) { return false; // item qt representation explicitly hidden } // the item (and theoretically all the parents) are not explicitly hidden // check the parent chain it = it->parent(); while (it) { if (it == d->mModel->rootItem()) { return true; // parent is root item: ok } // parent is not root item if (!isExpanded(d->mModel->index(it, 0))) { return false; // parent is not expanded (so child not actually visible) } it = it->parent(); // climb up } // parent hierarchy interrupted somewhere return false; } bool View::isThreaded() const { if (!d->mAggregation) { return false; } return d->mAggregation->threading() != Aggregation::NoThreading; } void View::slotSelectionChanged(const QItemSelection &, const QItemSelection &) { // We assume that when selection changes, current item also changes. QModelIndex current = currentIndex(); if (!current.isValid()) { d->mLastCurrentItem = nullptr; d->mWidget->viewMessageSelected(nullptr); d->mWidget->viewSelectionChanged(); return; } if (!selectionModel()->isSelected(current)) { if (selectedIndexes().count() < 1) { // It may happen after row removals: Model calls this slot on currentIndex() // that actually might have changed "silently", without being selected. QItemSelection selection; selection.append(QItemSelectionRange(current)); selectionModel()->select(selection, QItemSelectionModel::Select | QItemSelectionModel::Rows); return; // the above recurses } else { // something is still selected anyway // This is probably a result of CTRL+Click which unselected current: leave it as it is. return; } } Item *it = static_cast< Item * >(current.internalPointer()); Q_ASSERT(it); switch (it->type()) { case Item::Message: if (d->mLastCurrentItem != it) { qCDebug(MESSAGELIST_LOG) << "View message selected [" << static_cast< MessageItem * >(it)->subject() << "]"; d->mWidget->viewMessageSelected(static_cast< MessageItem * >(it)); d->mLastCurrentItem = it; } break; case Item::GroupHeader: if (d->mLastCurrentItem) { d->mWidget->viewMessageSelected(nullptr); d->mLastCurrentItem = nullptr; } break; default: // should never happen Q_ASSERT(false); break; } d->mWidget->viewSelectionChanged(); } void View::mouseDoubleClickEvent(QMouseEvent *e) { // Perform a hit test if (!d->mDelegate->hitTest(e->pos(), true)) { return; } // Something was hit :) Item *it = static_cast< Item * >(d->mDelegate->hitItem()); if (!it) { return; // should never happen } switch (it->type()) { case Item::Message: // Let QTreeView handle the expansion QTreeView::mousePressEvent(e); switch (e->button()) { case Qt::LeftButton: if (d->mDelegate->hitContentItem()) { // Double clicking on clickable icons does NOT activate the message if (d->mDelegate->hitContentItem()->isIcon() && d->mDelegate->hitContentItem()->isClickable()) { return; } } d->mWidget->viewMessageActivated(static_cast< MessageItem * >(it)); break; default: // make gcc happy break; } break; case Item::GroupHeader: // Don't let QTreeView handle the selection (as it deselects the current messages) switch (e->button()) { case Qt::LeftButton: if (it->childItemCount() > 0) { // toggle expanded state setExpanded(d->mDelegate->hitIndex(), !isExpanded(d->mDelegate->hitIndex())); } break; default: // make gcc happy break; } break; default: // should never happen Q_ASSERT(false); break; } } void View::changeMessageStatusRead(MessageItem *it, bool read) { Akonadi::MessageStatus set = it->status(); Akonadi::MessageStatus unset = it->status(); if (read) { set.setRead(true); unset.setRead(false); } else { set.setRead(false); unset.setRead(true); } viewport()->update(); // This will actually request the widget to perform a status change on the storage. // The request will be then processed by the Model and the message will be updated again. d->mWidget->viewMessageStatusChangeRequest(it, set, unset); } void View::changeMessageStatus(MessageItem *it, Akonadi::MessageStatus set, Akonadi::MessageStatus unset) { // We first change the status of MessageItem itself. This will make the change // visible to the user even if the Model is actually in the middle of a long job (maybe it's loading) // and can't process the status change request immediately. // Here we actually desynchronize the cache and trust that the later call to // d->mWidget->viewMessageStatusChangeRequest() will really perform the status change on the storage. // Well... in KMail it will unless something is really screwed. Anyway, if it will not, at the next // load the status will be just unchanged: no animals will be harmed. qint32 stat = it->status().toQInt32(); stat |= set.toQInt32(); stat &= ~(unset.toQInt32()); Akonadi::MessageStatus status; status.fromQInt32(stat); it->setStatus(status); // Trigger an update so the immediate change will be shown to the user viewport()->update(); // This will actually request the widget to perform a status change on the storage. // The request will be then processed by the Model and the message will be updated again. d->mWidget->viewMessageStatusChangeRequest(it, set, unset); } void View::mousePressEvent(QMouseEvent *e) { d->mMousePressPosition = QPoint(); // Perform a hit test if (!d->mDelegate->hitTest(e->pos(), true)) { return; } // Something was hit :) Item *it = static_cast< Item * >(d->mDelegate->hitItem()); if (!it) { return; // should never happen } // Abort any pending message pre-selection as the user is probably // already navigating the view (so pre-selection would make his view jump // to an unexpected place). d->mModel->setPreSelectionMode(PreSelectNone); switch (it->type()) { case Item::Message: d->mMousePressPosition = e->pos(); switch (e->button()) { case Qt::LeftButton: // if we have multi selection then the meaning of hitting // the content item is quite unclear. if (d->mDelegate->hitContentItem() && (selectedIndexes().count() > 1)) { qCDebug(MESSAGELIST_LOG) << "Left hit with selectedIndexes().count() == " << selectedIndexes().count(); switch (d->mDelegate->hitContentItem()->type()) { case Theme::ContentItem::AnnotationIcon: static_cast< MessageItem * >(it)->editAnnotation(this); return; // don't select the item break; case Theme::ContentItem::ActionItemStateIcon: changeMessageStatus( static_cast< MessageItem * >(it), it->status().isToAct() ? Akonadi::MessageStatus() : Akonadi::MessageStatus::statusToAct(), it->status().isToAct() ? Akonadi::MessageStatus::statusToAct() : Akonadi::MessageStatus() ); return; // don't select the item break; case Theme::ContentItem::ImportantStateIcon: changeMessageStatus( static_cast< MessageItem * >(it), it->status().isImportant() ? Akonadi::MessageStatus() : Akonadi::MessageStatus::statusImportant(), it->status().isImportant() ? Akonadi::MessageStatus::statusImportant() : Akonadi::MessageStatus() ); return; // don't select the item case Theme::ContentItem::ReadStateIcon: changeMessageStatusRead(static_cast< MessageItem * >(it), it->status().isRead() ? false : true); return; break; case Theme::ContentItem::SpamHamStateIcon: changeMessageStatus( static_cast< MessageItem * >(it), it->status().isSpam() ? Akonadi::MessageStatus() : (it->status().isHam() ? Akonadi::MessageStatus::statusSpam() : Akonadi::MessageStatus::statusHam()), it->status().isSpam() ? Akonadi::MessageStatus::statusSpam() : (it->status().isHam() ? Akonadi::MessageStatus::statusHam() : Akonadi::MessageStatus()) ); return; // don't select the item break; case Theme::ContentItem::WatchedIgnoredStateIcon: changeMessageStatus( static_cast< MessageItem * >(it), it->status().isIgnored() ? Akonadi::MessageStatus() : (it->status().isWatched() ? Akonadi::MessageStatus::statusIgnored() : Akonadi::MessageStatus::statusWatched()), it->status().isIgnored() ? Akonadi::MessageStatus::statusIgnored() : (it->status().isWatched() ? Akonadi::MessageStatus::statusWatched() : Akonadi::MessageStatus()) ); return; // don't select the item break; default: // make gcc happy break; } } // Let QTreeView handle the selection and Q_EMIT the appropriate signals (slotSelectionChanged() may be called) QTreeView::mousePressEvent(e); break; case Qt::RightButton: // Let QTreeView handle the selection and Q_EMIT the appropriate signals (slotSelectionChanged() may be called) QTreeView::mousePressEvent(e); e->accept(); d->mWidget->viewMessageListContextPopupRequest(selectionAsMessageItemList(), viewport()->mapToGlobal(e->pos())); break; default: // make gcc happy break; } break; case Item::GroupHeader: { // Don't let QTreeView handle the selection (as it deselects the current messages) GroupHeaderItem *groupHeaderItem = static_cast< GroupHeaderItem * >(it); switch (e->button()) { case Qt::LeftButton: { QModelIndex index = d->mModel->index(groupHeaderItem, 0); if (index.isValid()) { setCurrentIndex(index); } if (!d->mDelegate->hitContentItem()) { return; } if (d->mDelegate->hitContentItem()->type() == Theme::ContentItem::ExpandedStateIcon) { if (groupHeaderItem->childItemCount() > 0) { // toggle expanded state setExpanded(d->mDelegate->hitIndex(), !isExpanded(d->mDelegate->hitIndex())); } } break; } case Qt::RightButton: d->mWidget->viewGroupHeaderContextPopupRequest(groupHeaderItem, viewport()->mapToGlobal(e->pos())); break; default: // make gcc happy break; } break; } default: // should never happen Q_ASSERT(false); break; } } void View::mouseMoveEvent(QMouseEvent *e) { if (!(e->buttons() & Qt::LeftButton)) { QTreeView::mouseMoveEvent(e); return; } if (d->mMousePressPosition.isNull()) { return; } if ((e->pos() - d->mMousePressPosition).manhattanLength() <= QApplication::startDragDistance()) { return; } d->mWidget->viewStartDragRequest(); } #if 0 void View::contextMenuEvent(QContextMenuEvent *e) { Q_UNUSED(e); QModelIndex index = currentIndex(); if (index.isValid()) { QRect indexRect = this->visualRect(index); QPoint pos; if ((indexRect.isValid()) && (indexRect.bottom() > 0)) { if (indexRect.bottom() > viewport()->height()) { if (indexRect.top() <= viewport()->height()) { pos = indexRect.topLeft(); } } else { pos = indexRect.bottomLeft(); } } Item *item = static_cast< Item * >(index.internalPointer()); if (item) { if (item->type() == Item::GroupHeader) { d->mWidget->viewGroupHeaderContextPopupRequest(static_cast< GroupHeaderItem * >(item), viewport()->mapToGlobal(pos)); } else if (!selectionEmpty()) { d->mWidget->viewMessageListContextPopupRequest(selectionAsMessageItemList(), viewport()->mapToGlobal(pos)); e->accept(); } } } } #endif void View::dragEnterEvent(QDragEnterEvent *e) { d->mWidget->viewDragEnterEvent(e); } void View::dragMoveEvent(QDragMoveEvent *e) { d->mWidget->viewDragMoveEvent(e); } void View::dropEvent(QDropEvent *e) { d->mWidget->viewDropEvent(e); } void View::changeEvent(QEvent *e) { switch (e->type()) { case QEvent::FontChange: d->mDelegate->generalFontChanged(); Q_FALLTHROUGH(); case QEvent::PaletteChange: case QEvent::StyleChange: case QEvent::LayoutDirectionChange: case QEvent::LocaleChange: case QEvent::LanguageChange: // All of these affect the theme's internal cache. setTheme(d->mTheme); // A layoutChanged() event will screw up the view state a bit. // Since this is a rare event we just reload the view. reload(); break; default: // make gcc happy by default break; } QTreeView::changeEvent(e); } bool View::event(QEvent *e) { // We catch ToolTip events and pass everything else if (e->type() != QEvent::ToolTip) { return QTreeView::event(e); } if (!MessageListSettings::self()->messageToolTipEnabled()) { return true; // don't display tooltips } QHelpEvent *he = dynamic_cast< QHelpEvent * >(e); if (!he) { return true; // eh ? } QPoint pnt = viewport()->mapFromGlobal(mapToGlobal(he->pos())); if (pnt.y() < 0) { return true; // don't display the tooltip for items hidden under the header } QModelIndex idx = indexAt(pnt); if (!idx.isValid()) { return true; // may be } Item *it = static_cast< Item * >(idx.internalPointer()); if (!it) { return true; // hum } Q_ASSERT(storageModel()); QColor bckColor = palette().color(QPalette::ToolTipBase); QColor txtColor = palette().color(QPalette::ToolTipText); QColor darkerColor( ((bckColor.red() * 8) + (txtColor.red() * 2)) / 10, ((bckColor.green() * 8) + (txtColor.green() * 2)) / 10, ((bckColor.blue() * 8) + (txtColor.blue() * 2)) / 10 ); QString bckColorName = bckColor.name(); QString txtColorName = txtColor.name(); QString darkerColorName = darkerColor.name(); const bool textIsLeftToRight = (QApplication::layoutDirection() == Qt::LeftToRight); const QString textDirection = textIsLeftToRight ? QStringLiteral("left") : QStringLiteral("right"); QString tip = QStringLiteral( "" ); switch (it->type()) { case Item::Message: { MessageItem *mi = static_cast< MessageItem * >(it); tip += QStringLiteral( "" \ "" \ "" ).arg(txtColorName, bckColorName, mi->subject().toHtmlEscaped(), textDirection); tip += QLatin1String( "" \ "" \ "" ); // FIXME: Find a way to show also CC and other header fields ? if (mi->hasChildren()) { Item::ChildItemStats stats; mi->childItemStats(stats); QString statsText; statsText = i18np("%1 reply", "%1 replies", mi->childItemCount()); statsText += QLatin1String(", "); statsText += i18np( "%1 message in subtree (%2 unread)", "%1 messages in subtree (%2 unread)", stats.mTotalChildCount, stats.mUnreadChildCount ); tip += QStringLiteral( "" \ "" \ "" ).arg(darkerColorName).arg(statsText).arg(textDirection); } break; } case Item::GroupHeader: { GroupHeaderItem *ghi = static_cast< GroupHeaderItem * >(it); tip += QStringLiteral( "" \ "" \ "" ).arg(txtColorName).arg(bckColorName).arg(ghi->label()).arg(textDirection); QString description; switch (d->mAggregation->grouping()) { case Aggregation::GroupByDate: if (d->mAggregation->threading() != Aggregation::NoThreading) { switch (d->mAggregation->threadLeader()) { case Aggregation::TopmostMessage: if (ghi->label().contains(QRegularExpression(QStringLiteral("[0-9]")))) { description = i18nc( "@info:tooltip Formats to something like 'Threads started on 2008-12-21'", "Threads started on %1", ghi->label() ); } else { description = i18nc( "@info:tooltip Formats to something like 'Threads started Yesterday'", "Threads started %1", ghi->label() ); } break; case Aggregation::MostRecentMessage: description = i18n("Threads with messages dated %1", ghi->label()); break; default: // nuthin, make gcc happy break; } } else { if (ghi->label().contains(QRegularExpression(QStringLiteral("[0-9]")))) { if (storageModel()->containsOutboundMessages()) { description = i18nc( "@info:tooltip Formats to something like 'Messages sent on 2008-12-21'", "Messages sent on %1", ghi->label() ); } else { description = i18nc( "@info:tooltip Formats to something like 'Messages received on 2008-12-21'", "Messages received on %1", ghi->label() ); } } else { if (storageModel()->containsOutboundMessages()) { description = i18nc( "@info:tooltip Formats to something like 'Messages sent Yesterday'", "Messages sent %1", ghi->label() ); } else { description = i18nc( "@info:tooltip Formats to something like 'Messages received Yesterday'", "Messages received %1", ghi->label() ); } } } break; case Aggregation::GroupByDateRange: if (d->mAggregation->threading() != Aggregation::NoThreading) { switch (d->mAggregation->threadLeader()) { case Aggregation::TopmostMessage: description = i18n("Threads started within %1", ghi->label()); break; case Aggregation::MostRecentMessage: description = i18n("Threads containing messages with dates within %1", ghi->label()); break; default: // nuthin, make gcc happy break; } } else { if (storageModel()->containsOutboundMessages()) { description = i18n("Messages sent within %1", ghi->label()); } else { description = i18n("Messages received within %1", ghi->label()); } } break; case Aggregation::GroupBySenderOrReceiver: case Aggregation::GroupBySender: if (d->mAggregation->threading() != Aggregation::NoThreading) { switch (d->mAggregation->threadLeader()) { case Aggregation::TopmostMessage: description = i18n("Threads started by %1", ghi->label()); break; case Aggregation::MostRecentMessage: description = i18n("Threads with most recent message by %1", ghi->label()); break; default: // nuthin, make gcc happy break; } } else { if (storageModel()->containsOutboundMessages()) { if (d->mAggregation->grouping() == Aggregation::GroupBySenderOrReceiver) { description = i18n("Messages sent to %1", ghi->label()); } else { description = i18n("Messages sent by %1", ghi->label()); } } else { description = i18n("Messages received from %1", ghi->label()); } } break; case Aggregation::GroupByReceiver: if (d->mAggregation->threading() != Aggregation::NoThreading) { switch (d->mAggregation->threadLeader()) { case Aggregation::TopmostMessage: description = i18n("Threads directed to %1", ghi->label()); break; case Aggregation::MostRecentMessage: description = i18n("Threads with most recent message directed to %1", ghi->label()); break; default: // nuthin, make gcc happy break; } } else { if (storageModel()->containsOutboundMessages()) { description = i18n("Messages sent to %1", ghi->label()); } else { description = i18n("Messages received by %1", ghi->label()); } } break; default: // nuthin, make gcc happy break; } if (!description.isEmpty()) { tip += QStringLiteral( "" \ "" \ "" ).arg(description).arg(textDirection); } if (ghi->hasChildren()) { Item::ChildItemStats stats; ghi->childItemStats(stats); QString statsText; if (d->mAggregation->threading() != Aggregation::NoThreading) { statsText = i18np("%1 thread", "%1 threads", ghi->childItemCount()); statsText += QLatin1String(", "); } statsText += i18np( "%1 message (%2 unread)", "%1 messages (%2 unread)", stats.mTotalChildCount, stats.mUnreadChildCount ); tip += QStringLiteral( "" \ "" \ "" ).arg(darkerColorName).arg(statsText).arg(textDirection); } break; } default: // nuthin (just make gcc happy for now) break; } tip += QLatin1String( "
    " \ "
    " \ "%3" \ "
    " \ "
    " \ "" ); const QString htmlCodeForStandardRow = QStringLiteral( "" \ "" \ "" \ ""); if (textIsLeftToRight) { tip += htmlCodeForStandardRow.arg(i18n("From"), mi->displaySender().toHtmlEscaped()); tip += htmlCodeForStandardRow.arg(i18nc("Receiver of the email", "To"), mi->displayReceiver().toHtmlEscaped()); tip += htmlCodeForStandardRow.arg(i18n("Date"), mi->formattedDate()); } else { tip += htmlCodeForStandardRow.arg(mi->displaySender().toHtmlEscaped(), i18n("From")); tip += htmlCodeForStandardRow.arg(mi->displayReceiver().toHtmlEscaped(), i18nc("Receiver of the email", "To")); tip += htmlCodeForStandardRow.arg(mi->formattedDate(), i18n("Date")); } QString status = mi->statusDescription(); const QString tags = mi->tagListDescription(); if (!tags.isEmpty()) { if (!status.isEmpty()) { status += QLatin1String(", "); } status += tags; } if (textIsLeftToRight) { - tip += htmlCodeForStandardRow.arg(i18n("Status")).arg(status); - tip += htmlCodeForStandardRow.arg(i18n("Size")).arg(mi->formattedSize()); - tip += htmlCodeForStandardRow.arg(i18n("Folder")).arg(mi->folder()); + tip += htmlCodeForStandardRow.arg(i18n("Status"), status); + tip += htmlCodeForStandardRow.arg(i18n("Size"), mi->formattedSize()); + tip += htmlCodeForStandardRow.arg(i18n("Folder"), mi->folder()); } else { - tip += htmlCodeForStandardRow.arg(status).arg(i18n("Status")); - tip += htmlCodeForStandardRow.arg(mi->formattedSize()).arg(i18n("Size")); - tip += htmlCodeForStandardRow.arg(mi->folder()).arg(i18n("Folder")); + tip += htmlCodeForStandardRow.arg(status, i18n("Status")); + tip += htmlCodeForStandardRow.arg(mi->formattedSize(), i18n("Size")); + tip += htmlCodeForStandardRow.arg(mi->folder(), i18n("Folder")); } if (mi->hasAnnotation()) { if (textIsLeftToRight) { tip += htmlCodeForStandardRow.arg(i18n("Note"), mi->annotation().replace(QLatin1Char('\n'), QStringLiteral("
    "))); } else { tip += htmlCodeForStandardRow.arg(mi->annotation().replace(QLatin1Char('\n'), QStringLiteral("
    "))).arg(i18n("Note")); } } QString content = MessageList::Util::contentSummary(mi->akonadiItem()); if (!content.trimmed().isEmpty()) { if (textIsLeftToRight) { tip += htmlCodeForStandardRow.arg(i18n("Preview"), content.replace(QLatin1Char('\n'), QStringLiteral("
    "))); } else { tip += htmlCodeForStandardRow.arg(content.replace(QLatin1Char('\n'), QStringLiteral("
    "))).arg(i18n("Preview")); } } tip += QLatin1String( "
    " \ "
    " \ "%1:" \ "
    " \ "
    " \ "%2" \ "
    " \ "
    " \ "%2" \ "
    " \ "
    " \ "%3" \ "
    " \ "
    " \ "%1" \ "
    " \ "%2" \ "
    " ); QToolTip::showText(he->globalPos(), tip, viewport(), visualRect(idx)); return true; } void View::slotCollapseAllGroups() { setAllGroupsExpanded(false); } void View::slotExpandAllGroups() { setAllGroupsExpanded(true); } void View::slotCollapseCurrentItem() { setCurrentThreadExpanded(false); } void View::slotExpandCurrentItem() { setCurrentThreadExpanded(true); } void View::focusQuickSearch(const QString &selectedText) { d->mWidget->focusQuickSearch(selectedText); } QVector View::currentFilterStatus() const { return d->mWidget->currentFilterStatus(); } MessageList::Core::QuickSearchLine::SearchOptions View::currentOptions() const { return d->mWidget->currentOptions(); } QString View::currentFilterSearchString() const { return d->mWidget->currentFilterSearchString(); } void View::setRowHidden(int row, const QModelIndex &parent, bool hide) { const QModelIndex rowModelIndex = model()->index(row, 0, parent); const Item *const rowItem = static_cast< Item * >(rowModelIndex.internalPointer()); if (rowItem) { const bool currentlyHidden = isRowHidden(row, parent); if (currentlyHidden != hide) { if (currentMessageItem() == rowItem) { selectionModel()->clear(); selectionModel()->clearSelection(); } } } QTreeView::setRowHidden(row, parent, hide); } void View::sortOrderMenuAboutToShow(QMenu *menu) { d->mWidget->sortOrderMenuAboutToShow(menu); } void View::aggregationMenuAboutToShow(QMenu *menu) { d->mWidget->aggregationMenuAboutToShow(menu); } void View::themeMenuAboutToShow(QMenu *menu) { d->mWidget->themeMenuAboutToShow(menu); } void View::setCollapseItem(const QModelIndex &index) { if (index.isValid()) { setExpanded(index, false); } } void View::setExpandItem(const QModelIndex &index) { if (index.isValid()) { setExpanded(index, true); } } void View::setQuickSearchClickMessage(const QString &msg) { d->mWidget->quickSearch()->setPlaceholderText(msg); } #include "moc_view.cpp" diff --git a/messageviewer/src/dkim-verify/dkimauthenticationstatusinfo.cpp b/messageviewer/src/dkim-verify/dkimauthenticationstatusinfo.cpp index 642f31a5..049d7aeb 100644 --- a/messageviewer/src/dkim-verify/dkimauthenticationstatusinfo.cpp +++ b/messageviewer/src/dkim-verify/dkimauthenticationstatusinfo.cpp @@ -1,277 +1,277 @@ /* Copyright (C) 2018-2020 Laurent Montel 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 "dkimauthenticationstatusinfo.h" #include "dkimauthenticationstatusinfoutil.h" #include "messageviewer_dkimcheckerdebug.h" #include using namespace MessageViewer; //see https://tools.ietf.org/html/rfc7601 DKIMAuthenticationStatusInfo::DKIMAuthenticationStatusInfo() { } bool DKIMAuthenticationStatusInfo::parseAuthenticationStatus(const QString &key, bool relaxingParsing) { QString valueKey = key; //kmime remove extra \r\n but we need it for regexp at the end. if (!valueKey.endsWith(QLatin1String("\r\n"))) { valueKey += QLatin1String("\r\n"); } // https://tools.ietf.org/html/rfc7601#section-2.2 // authres-header = "Authentication-Results:" [CFWS] authserv-id // [ CFWS authres-version ] // ( no-result / 1*resinfo ) [CFWS] CRLF // 1) extract AuthservId and AuthVersion QRegularExpressionMatch match; const QString regStr = DKIMAuthenticationStatusInfoUtil::value_cp() + QLatin1String("(?:") + DKIMAuthenticationStatusInfoUtil::cfws_p() + QLatin1String("([0-9]+)") + DKIMAuthenticationStatusInfoUtil::cfws_op() + QLatin1String(" )?"); //qDebug() << " regStr" << regStr; static const QRegularExpression regular1(regStr); int index = valueKey.indexOf(regular1, 0, &match); if (index != -1) { mAuthservId = match.captured(1); const QString authVersionStr = match.captured(2); if (!authVersionStr.isEmpty()) { mAuthVersion = authVersionStr.toInt(); } else { mAuthVersion = 1; } valueKey = valueKey.right(valueKey.length() - (index + match.capturedLength(0))); //qDebug() << " match.captured(0)"<@,;:\\\\\"[\\]?=]+"); } const QString property_p = QLatin1String("mailfrom|rcptto") + QLatin1Char('|') + DKIMAuthenticationStatusInfoUtil::keyword_p(); const QString propspec_p = QLatin1Char('(') + DKIMAuthenticationStatusInfoUtil::keyword_p() + QLatin1Char(')') + DKIMAuthenticationStatusInfoUtil::cfws_op() + QLatin1String("\\.") + DKIMAuthenticationStatusInfoUtil::cfws_op() + QLatin1Char('(') + property_p + QLatin1Char(')') + DKIMAuthenticationStatusInfoUtil::cfws_op() + QLatin1Char('=') + DKIMAuthenticationStatusInfoUtil::cfws_op() + QLatin1Char('(') + pvalue_p /*+ QLatin1Char(')')*/; //qDebug() << "propspec_p " << propspec_p; const QString regexp = DKIMAuthenticationStatusInfoUtil::regexMatchO(propspec_p); static const QRegularExpression reg(regexp); if (!reg.isValid()) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << " reg error : " << reg.errorString(); } else { index = valueKey.indexOf(reg, 0, &match); while (index != -1) { //qDebug() << " propspec " << match.capturedTexts(); valueKey = valueKey.right(valueKey.length() - (index + match.capturedLength(0))); // Improve it! //qDebug() << " value KEy " << valueKey; const QString &captured1 = match.captured(1); //qDebug() << " captured1 " << captured1; if (captured1 == QLatin1String("header")) { AuthStatusInfo::Property prop; prop.type = match.captured(2); prop.value = match.captured(3); authStatusInfo.header.append(prop); } else if (captured1 == QLatin1String("smtp")) { AuthStatusInfo::Property prop; prop.type = match.captured(2); prop.value = match.captured(3); authStatusInfo.smtp.append(prop); } else if (captured1 == QLatin1String("body")) { AuthStatusInfo::Property prop; prop.type = match.captured(2); prop.value = match.captured(3); authStatusInfo.body.append(prop); } else if (captured1 == QLatin1String("policy")) { AuthStatusInfo::Property prop; prop.type = match.captured(2); prop.value = match.captured(3); authStatusInfo.policy.append(prop); } else { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Unknow type found " << captured1; } index = valueKey.indexOf(reg, 0, &match); } } return authStatusInfo; } int DKIMAuthenticationStatusInfo::authVersion() const { return mAuthVersion; } void DKIMAuthenticationStatusInfo::setAuthVersion(int authVersion) { mAuthVersion = authVersion; } QString DKIMAuthenticationStatusInfo::reasonSpec() const { return mReasonSpec; } void DKIMAuthenticationStatusInfo::setReasonSpec(const QString &reasonSpec) { mReasonSpec = reasonSpec; } bool DKIMAuthenticationStatusInfo::operator==(const DKIMAuthenticationStatusInfo &other) const { return mAuthservId == other.authservId() && mAuthVersion == other.authVersion() && mReasonSpec == other.reasonSpec() && mListAuthStatusInfo == other.listAuthStatusInfo(); } QVector DKIMAuthenticationStatusInfo::listAuthStatusInfo() const { return mListAuthStatusInfo; } void DKIMAuthenticationStatusInfo::setListAuthStatusInfo(const QVector &listAuthStatusInfo) { mListAuthStatusInfo = listAuthStatusInfo; } QString DKIMAuthenticationStatusInfo::authservId() const { return mAuthservId; } void DKIMAuthenticationStatusInfo::setAuthservId(const QString &authservId) { mAuthservId = authservId; } QDebug operator <<(QDebug d, const DKIMAuthenticationStatusInfo &t) { d << "mAuthservId: " << t.authservId(); d << "mReasonSpec: " << t.reasonSpec(); d << "mAuthVersion: " << t.authVersion() << '\n'; const auto listAuthStatusInfo = t.listAuthStatusInfo(); for (const DKIMAuthenticationStatusInfo::AuthStatusInfo &info : listAuthStatusInfo) { d << "mListAuthStatusInfo: " << info.method << " : " << info.result << " : " << info.methodVersion << " : " << info.reason << '\n'; d << "Property:" << '\n'; if (!info.smtp.isEmpty()) { for (const DKIMAuthenticationStatusInfo::AuthStatusInfo::Property &prop : info.smtp) { d << " smtp " << prop.type << " : " << prop.value << '\n'; } } if (!info.header.isEmpty()) { for (const DKIMAuthenticationStatusInfo::AuthStatusInfo::Property &prop : info.header) { d << " header " << prop.type << " : " << prop.value << '\n'; } } if (!info.body.isEmpty()) { for (const DKIMAuthenticationStatusInfo::AuthStatusInfo::Property &prop : info.body) { d << " body " << prop.type << " : " << prop.value << '\n'; } } if (!info.policy.isEmpty()) { for (const DKIMAuthenticationStatusInfo::AuthStatusInfo::Property &prop : info.policy) { d << " policy " << prop.type << " : " << prop.value << '\n'; } } } return d; } bool DKIMAuthenticationStatusInfo::AuthStatusInfo::operator==(const DKIMAuthenticationStatusInfo::AuthStatusInfo &other) const { return other.method == method && other.result == result && other.methodVersion == methodVersion && other.reason == reason && other.policy == policy && other.smtp == smtp && other.header == header && other.body == body; } bool DKIMAuthenticationStatusInfo::AuthStatusInfo::isValid() const { return !method.isEmpty(); } diff --git a/messageviewer/src/dkim-verify/dkimauthenticationstatusinfoutil.cpp b/messageviewer/src/dkim-verify/dkimauthenticationstatusinfoutil.cpp index 0ef2c2d7..6d6a59f5 100644 --- a/messageviewer/src/dkim-verify/dkimauthenticationstatusinfoutil.cpp +++ b/messageviewer/src/dkim-verify/dkimauthenticationstatusinfoutil.cpp @@ -1,209 +1,209 @@ /* Copyright (C) 2019-2020 Laurent Montel Code based on ARHParser.jsm from dkim_verifier (Copyright (c) Philippe Lieser) (This software is licensed under the terms of the MIT License.) 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 "dkimauthenticationstatusinfoutil.h" /* // domain-name as specified in Section 3.5 of RFC 6376 [DKIM]. let domain_name_p = "(?:" + sub_domain_p + "(?:\\." + sub_domain_p + ")+)"; */ QString MessageViewer::DKIMAuthenticationStatusInfoUtil::wsp_p() { // WSP as specified in Appendix B.1 of RFC 5234 return QStringLiteral("[ \t]"); } QString MessageViewer::DKIMAuthenticationStatusInfoUtil::vchar_p() { // VCHAR as specified in Appendix B.1 of RFC 5234 return QStringLiteral("[!-~]"); } QString MessageViewer::DKIMAuthenticationStatusInfoUtil::letDig_p() { // Let-dig as specified in Section 4.1.2 of RFC 5321 [SMTP]. return QStringLiteral("[A-Za-z0-9]"); } QString MessageViewer::DKIMAuthenticationStatusInfoUtil::ldhStr_p() { // Ldh-str as specified in Section 4.1.2 of RFC 5321 [SMTP]. return QStringLiteral("(?:[A-Za-z0-9-]*%1)").arg(DKIMAuthenticationStatusInfoUtil::letDig_p()); } QString MessageViewer::DKIMAuthenticationStatusInfoUtil::keyword_p() { // "Keyword" as specified in Section 4.1.2 of RFC 5321 [SMTP]. return DKIMAuthenticationStatusInfoUtil::ldhStr_p(); } QString MessageViewer::DKIMAuthenticationStatusInfoUtil::subDomain_p() { // sub-domain as specified in Section 4.1.2 of RFC 5321 [SMTP]. return QStringLiteral("(?:%1%2?)").arg(DKIMAuthenticationStatusInfoUtil::letDig_p(), DKIMAuthenticationStatusInfoUtil::ldhStr_p()); } QString MessageViewer::DKIMAuthenticationStatusInfoUtil::obsFws_p() { // obs-FWS as specified in Section 4.2 of RFC 5322 return QStringLiteral("(?:%1+(?:\r\n%1+)*)").arg(DKIMAuthenticationStatusInfoUtil::wsp_p()); } QString MessageViewer::DKIMAuthenticationStatusInfoUtil::quotedPair_p() { // quoted-pair as specified in Section 3.2.1 of RFC 5322 // Note: obs-qp is not included, so this pattern matches less then specified! return QStringLiteral("(?:\\\\(?:%1|%2))").arg(vchar_p(), wsp_p()); } QString MessageViewer::DKIMAuthenticationStatusInfoUtil::fws_p() { // FWS as specified in Section 3.2.2 of RFC 5322 return QStringLiteral("(?:(?:(?:%1*\r\n)?%1+)|%2)").arg(wsp_p(), obsFws_p()); } QString MessageViewer::DKIMAuthenticationStatusInfoUtil::fws_op() { return QStringLiteral("%1?").arg(fws_p()); } QString MessageViewer::DKIMAuthenticationStatusInfoUtil::ctext_p() { // ctext as specified in Section 3.2.2 of RFC 5322 return QStringLiteral("[!-'*-[\\]-~]"); } QString MessageViewer::DKIMAuthenticationStatusInfoUtil::ccontent_p() { // ccontent as specified in Section 3.2.2 of RFC 5322 // Note: comment is not included, so this pattern matches less then specified! return QStringLiteral("(?:%1|%2)").arg(ctext_p(), quotedPair_p()); } QString MessageViewer::DKIMAuthenticationStatusInfoUtil::comment_p() { // comment as specified in Section 3.2.2 of RFC 5322 return QStringLiteral("\\((?:%1%2)*%1\\)").arg(fws_op(), ccontent_p()); } QString MessageViewer::DKIMAuthenticationStatusInfoUtil::cfws_p() { // CFWS as specified in Section 3.2.2 of RFC 5322 [MAIL] return QStringLiteral("(?:(?:(?:%1%2)+%1)|%3)").arg(fws_op(), comment_p(), fws_p()); } QString MessageViewer::DKIMAuthenticationStatusInfoUtil::cfws_op() { return QStringLiteral("%1?").arg(cfws_p()); } QString MessageViewer::DKIMAuthenticationStatusInfoUtil::atext() { // atext as specified in Section 3.2.3 of RFC 5322 return QStringLiteral("[!#-'*-+/-9=?A-Z^-~-]"); } QString MessageViewer::DKIMAuthenticationStatusInfoUtil::dotAtomText_p() { // dot-atom-text as specified in Section 3.2.3 of RFC 5322 return QStringLiteral("(?:%1+(?:\\.%1+)*)").arg(atext()); } QString MessageViewer::DKIMAuthenticationStatusInfoUtil::dotAtom_p() { // dot-atom as specified in Section 3.2.3 of RFC 5322 // dot-atom = [CFWS] dot-atom-text [CFWS] return QStringLiteral("(?:%1%2%1)").arg(cfws_op(), dotAtomText_p()); } QString MessageViewer::DKIMAuthenticationStatusInfoUtil::qtext_p() { // qtext as specified in Section 3.2.4 of RFC 5322 // Note: obs-qtext is not included, so this pattern matches less then specified! return QStringLiteral("[!#-[\\]-~]"); } QString MessageViewer::DKIMAuthenticationStatusInfoUtil::qcontent_p() { // qcontent as specified in Section 3.2.4 of RFC 5322 - return QStringLiteral("(?:%1|%2)").arg(qtext_p()).arg(quotedPair_p()); + return QStringLiteral("(?:%1|%2)").arg(qtext_p(), quotedPair_p()); } QString MessageViewer::DKIMAuthenticationStatusInfoUtil::quotedString_p() { // quoted-string as specified in Section 3.2.4 of RFC 5322 return QStringLiteral("(?:%1\"(?:%2%3)*%2\"%1)").arg(cfws_op(), fws_op(), qcontent_p()); } QString MessageViewer::DKIMAuthenticationStatusInfoUtil::quotedString_cp() { return QStringLiteral("(?:%1\"((?:%2%3)*)%2\"%1)").arg(cfws_op(), fws_op(), qcontent_p()); } QString MessageViewer::DKIMAuthenticationStatusInfoUtil::localPart_p() { // local-part as specified in Section 3.4.1 of RFC 5322 // Note: obs-local-part is not included, so this pattern matches less then specified! return QStringLiteral("(?:%1|%2))").arg(dotAtom_p(), quotedString_p()); } QString MessageViewer::DKIMAuthenticationStatusInfoUtil::token_p() { // token as specified in Section 5.1 of RFC 2045. return QStringLiteral("[^ \\x00-\\x1F\\x7F()<>@,;:\\\\\"/[\\]?=]+"); } QString MessageViewer::DKIMAuthenticationStatusInfoUtil::value_p() { // "value" as specified in Section 5.1 of RFC 2045. return QStringLiteral("(?:%1|%2)").arg(token_p(), quotedString_p()); } QString MessageViewer::DKIMAuthenticationStatusInfoUtil::value_cp() { return QStringLiteral("(?:(%1)|%2)").arg(token_p(), quotedString_cp()); } QString MessageViewer::DKIMAuthenticationStatusInfoUtil::domainName_p() { // domain-name as specified in Section 3.5 of RFC 6376 [DKIM]. return QStringLiteral("(?:%1(?:\\.%1)+)").arg(subDomain_p()); } // Tries to matches a pattern to the beginning of str. // Adds CFWS_op to the beginning of pattern. // pattern must be followed by string end, ";" or CFWS_p. // If match is found, removes it from str. QString MessageViewer::DKIMAuthenticationStatusInfoUtil::regexMatchO(const QString ®ularExpressionStr) { const QString regexp = (QLatin1Char('^') + DKIMAuthenticationStatusInfoUtil::cfws_op() + QStringLiteral("(?:") + regularExpressionStr + QLatin1Char(')') + QStringLiteral("(?:(?:") + DKIMAuthenticationStatusInfoUtil::cfws_op() + QStringLiteral("\r\n$)|(?=;)|(?=") + DKIMAuthenticationStatusInfoUtil::cfws_p() + QStringLiteral("))")); return regexp; } diff --git a/messageviewer/src/dkim-verify/dkimcheckpolicyjob.cpp b/messageviewer/src/dkim-verify/dkimcheckpolicyjob.cpp index 5042c8b0..87d3ba0c 100644 --- a/messageviewer/src/dkim-verify/dkimcheckpolicyjob.cpp +++ b/messageviewer/src/dkim-verify/dkimcheckpolicyjob.cpp @@ -1,154 +1,155 @@ /* Copyright (C) 2019-2020 Laurent Montel 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 "dkimcheckpolicyjob.h" #include "dmarcpolicyjob.h" #include "dkim-verify/dkimmanagerrules.h" #include "settings/messageviewersettings.h" #include "messageviewer_dkimcheckerdebug.h" #include "dkim-verify/dmarcmanager.h" using namespace MessageViewer; DKIMCheckPolicyJob::DKIMCheckPolicyJob(QObject *parent) : QObject(parent) { } DKIMCheckPolicyJob::~DKIMCheckPolicyJob() { } bool DKIMCheckPolicyJob::canStart() const { return !mEmailAddress.isEmpty(); } bool DKIMCheckPolicyJob::start() { if (!canStart()) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Impossible to start DKIMCheckPolicyJob" << mEmailAddress; Q_EMIT result(mCheckResult); deleteLater(); return false; } if (mPolicy.useDMarc()) { if (DMARCManager::self()->isNoDMarcServerAddress(mEmailAddress)) { Q_EMIT result(mCheckResult); deleteLater(); return true; } DMARCPolicyJob *job = new DMARCPolicyJob(this); job->setEmailAddress(mEmailAddress); connect(job, &DMARCPolicyJob::result, this, &DKIMCheckPolicyJob::dmarcPolicyResult); if (!job->start()) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Impossible to start DKIMCheckPolicyJob" << mEmailAddress; Q_EMIT result(mCheckResult); deleteLater(); return false; } } else { if (mPolicy.useDefaultRules()) { compareWithDefaultRules(); } else { Q_EMIT result(mCheckResult); deleteLater(); } } return true; } void DKIMCheckPolicyJob::compareWithDefaultRules() { const QVector rules = DKIMManagerRules::self()->rules(); for (const DKIMRule &rule : rules) { if (rule.enabled()) { if (rule.from() == mEmailAddress || rule.from() == QLatin1Char('*')) { //Check SDID - for (const QString &ssid : rule.signedDomainIdentifier()) { + const QStringList signedDomainIdentifier = rule.signedDomainIdentifier(); + for (const QString &ssid : signedDomainIdentifier) { if (mCheckResult.sdid == ssid) { switch (rule.ruleType()) { case DKIMRule::RuleType::Unknown: // Invalid rule ! qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Invalid rule found " << rule; break; case DKIMRule::RuleType::MustBeSigned: mCheckResult.status = DKIMCheckSignatureJob::DKIMStatus::NeedToBeSigned; break; case DKIMRule::RuleType::CanBeSigned: //Show a warning ? break; case DKIMRule::RuleType::IgnoreEmailNotSigned: //Nothing ! break; } break; } } } } } Q_EMIT result(mCheckResult); deleteLater(); } void DKIMCheckPolicyJob::dmarcPolicyResult(const MessageViewer::DMARCPolicyJob::DMARCResult &value, const QString &emailAddress) { if (value.isValid()) { if (mCheckResult.status == DKIMCheckSignatureJob::DKIMStatus::EmailNotSigned) { mCheckResult.status = DKIMCheckSignatureJob::DKIMStatus::NeedToBeSigned; //qDebug() << " void DKIMCheckPolicyJob::dmarcPolicyResult(const MessageViewer::DMARCPolicyJob::DMARCResult &value)"<addNoDMarcServerAddress(emailAddress); } Q_EMIT result(mCheckResult); deleteLater(); } MessageViewer::DKIMCheckSignatureJob::CheckSignatureResult DKIMCheckPolicyJob::checkResult() const { return mCheckResult; } void DKIMCheckPolicyJob::setCheckResult(const MessageViewer::DKIMCheckSignatureJob::CheckSignatureResult &checkResult) { mCheckResult = checkResult; } QString DKIMCheckPolicyJob::emailAddress() const { return mEmailAddress; } void DKIMCheckPolicyJob::setEmailAddress(const QString &emailAddress) { mEmailAddress = emailAddress; } DKIMCheckPolicy DKIMCheckPolicyJob::policy() const { return mPolicy; } void DKIMCheckPolicyJob::setPolicy(const DKIMCheckPolicy &policy) { mPolicy = policy; } diff --git a/messageviewer/src/dkim-verify/dkimchecksignaturejob.cpp b/messageviewer/src/dkim-verify/dkimchecksignaturejob.cpp index b8428aeb..ef2f6dad 100644 --- a/messageviewer/src/dkim-verify/dkimchecksignaturejob.cpp +++ b/messageviewer/src/dkim-verify/dkimchecksignaturejob.cpp @@ -1,725 +1,731 @@ /* Copyright (C) 2018-2020 Laurent Montel 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 "dkimchecksignaturejob.h" #include "dkimdownloadkeyjob.h" #include "dkimmanagerkey.h" #include "dkiminfo.h" #include "dkimutil.h" #include "dkimkeyrecord.h" #include "messageviewer_dkimcheckerdebug.h" #include #include #include #include #include //see https://tools.ietf.org/html/rfc6376 //#define DEBUG_SIGNATURE_DKIM 1 using namespace MessageViewer; DKIMCheckSignatureJob::DKIMCheckSignatureJob(QObject *parent) : QObject(parent) { } DKIMCheckSignatureJob::~DKIMCheckSignatureJob() { } MessageViewer::DKIMCheckSignatureJob::CheckSignatureResult DKIMCheckSignatureJob::createCheckResult() { MessageViewer::DKIMCheckSignatureJob::CheckSignatureResult result; result.error = mError; result.warning = mWarning; result.status = mStatus; result.sdid = mDkimInfo.domain(); result.auid = mDkimInfo.agentOrUserIdentifier(); result.fromEmail = mFromEmail; result.listSignatureAuthenticationResult = mCheckSignatureAuthenticationResult; return result; } QString DKIMCheckSignatureJob::bodyCanonizationResult() const { return mBodyCanonizationResult; } QString DKIMCheckSignatureJob::headerCanonizationResult() const { return mHeaderCanonizationResult; } void DKIMCheckSignatureJob::start() { if (!mMessage) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Item has not a message"; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; } if (auto hrd = mMessage->headerByType("DKIM-Signature")) { mDkimValue = hrd->asUnicodeString(); } //Store mFromEmail before looking at mDkimValue value. Otherwise we can return a from empty if (auto hrd = mMessage->from(false)) { mFromEmail = KEmailAddress::extractEmailAddress(hrd->asUnicodeString()); } if (mDkimValue.isEmpty()) { mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::EmailNotSigned; Q_EMIT result(createCheckResult()); deleteLater(); return; } qCDebug(MESSAGEVIEWER_DKIMCHECKER_LOG) << "mFromEmail " << mFromEmail; if (!mDkimInfo.parseDKIM(mDkimValue)) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Impossible to parse header" << mDkimValue; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; } const MessageViewer::DKIMCheckSignatureJob::DKIMStatus status = checkSignature(mDkimInfo); if (status != MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Valid) { mStatus = status; Q_EMIT result(createCheckResult()); deleteLater(); return; } //ComputeBodyHash now. switch (mDkimInfo.bodyCanonization()) { case MessageViewer::DKIMInfo::CanonicalizationType::Unknown: mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InvalidBodyCanonicalization; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; case MessageViewer::DKIMInfo::CanonicalizationType::Simple: mBodyCanonizationResult = bodyCanonizationSimple(); break; case MessageViewer::DKIMInfo::CanonicalizationType::Relaxed: mBodyCanonizationResult = bodyCanonizationRelaxed(); break; } //qDebug() << " bodyCanonizationResult "<< mBodyCanonizationResult << " algorithm " << mDkimInfo.hashingAlgorithm() << mDkimInfo.bodyHash(); if (mDkimInfo.bodyLengthCount() != -1) { //Verify it. if (mDkimInfo.bodyLengthCount() > mBodyCanonizationResult.length()) { // length tag exceeds body size qCDebug(MESSAGEVIEWER_DKIMCHECKER_LOG) << " mDkimInfo.bodyLengthCount() " << mDkimInfo.bodyLengthCount() << " mBodyCanonizationResult.length() " << mBodyCanonizationResult.length(); mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::SignatureTooLarge; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; } else if (mDkimInfo.bodyLengthCount() < mBodyCanonizationResult.length()) { mWarning = MessageViewer::DKIMCheckSignatureJob::DKIMWarning::SignatureTooSmall; } // truncated body to the length specified in the "l=" tag mBodyCanonizationResult = mBodyCanonizationResult.left(mDkimInfo.bodyLengthCount()); } if (mBodyCanonizationResult.startsWith(QLatin1String("\r\n"))) { //Remove it from start mBodyCanonizationResult = mBodyCanonizationResult.right(mBodyCanonizationResult.length() -2); } #ifdef DEBUG_SIGNATURE_DKIM QFile caFile(QStringLiteral("/tmp/bodycanon-kmail.txt")); caFile.open(QIODevice::WriteOnly | QIODevice::Text); QTextStream outStream(&caFile); outStream << mBodyCanonizationResult; caFile.close(); #endif QByteArray resultHash; switch (mDkimInfo.hashingAlgorithm()) { case DKIMInfo::HashingAlgorithmType::Sha1: resultHash = MessageViewer::DKIMUtil::generateHash(mBodyCanonizationResult.toLatin1(), QCryptographicHash::Sha1); break; case DKIMInfo::HashingAlgorithmType::Sha256: resultHash = MessageViewer::DKIMUtil::generateHash(mBodyCanonizationResult.toLatin1(), QCryptographicHash::Sha256); break; case DKIMInfo::HashingAlgorithmType::Any: case DKIMInfo::HashingAlgorithmType::Unknown: mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InsupportedHashAlgorithm; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; } // compare body hash qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "resultHash " << resultHash << "mDkimInfo.bodyHash()" << mDkimInfo.bodyHash(); if (resultHash != mDkimInfo.bodyHash().toLatin1()) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << " Corrupted body hash"; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::CorruptedBodyHash; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; } if (mDkimInfo.headerCanonization() == MessageViewer::DKIMInfo::CanonicalizationType::Unknown) { mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InvalidHeaderCanonicalization; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; } //Parse message header if (!mHeaderParser.wasAlreadyParsed()) { mHeaderParser.setHead(mMessage->head()); mHeaderParser.parse(); } computeHeaderCanonization(true); if (mPolicy.saveKey() == MessageViewer::MessageViewerSettings::EnumSaveKey::Save) { const QString keyValue = MessageViewer::DKIMManagerKey::self()->keyValue(mDkimInfo.selector(), mDkimInfo.domain()); //qDebug() << " mDkimInfo.selector() " << mDkimInfo.selector() << "mDkimInfo.domain() " << mDkimInfo.domain() << keyValue; if (keyValue.isEmpty()) { downloadKey(mDkimInfo); } else { parseDKIMKeyRecord(keyValue, mDkimInfo.domain(), mDkimInfo.selector(), false); } } else { downloadKey(mDkimInfo); } } void DKIMCheckSignatureJob::computeHeaderCanonization(bool removeQuoteOnContentType) { //Compute Hash Header switch (mDkimInfo.headerCanonization()) { case MessageViewer::DKIMInfo::CanonicalizationType::Unknown: return; case MessageViewer::DKIMInfo::CanonicalizationType::Simple: mHeaderCanonizationResult = headerCanonizationSimple(); break; case MessageViewer::DKIMInfo::CanonicalizationType::Relaxed: mHeaderCanonizationResult = headerCanonizationRelaxed(removeQuoteOnContentType); break; } // In hash step 2, the Signer/Verifier MUST pass the following to the // hash algorithm in the indicated order. // 1. The header fields specified by the "h=" tag, in the order // specified in that tag, and canonicalized using the header // canonicalization algorithm specified in the "c=" tag. Each // header field MUST be terminated with a single CRLF. // 2. The DKIM-Signature header field that exists (verifying) or will // be inserted (signing) in the message, with the value of the "b=" // tag (including all surrounding whitespace) deleted (i.e., treated // as the empty string), canonicalized using the header // canonicalization algorithm specified in the "c=" tag, and without // a trailing CRLF. // add DKIM-Signature header to the hash input // with the value of the "b=" tag (including all surrounding whitespace) deleted //Add dkim-signature as lowercase QString dkimValue = mDkimValue; dkimValue = dkimValue.left(dkimValue.indexOf(QLatin1String("b=")) + 2); switch (mDkimInfo.headerCanonization()) { case MessageViewer::DKIMInfo::CanonicalizationType::Unknown: return; case MessageViewer::DKIMInfo::CanonicalizationType::Simple: mHeaderCanonizationResult += QLatin1String("\r\n") + MessageViewer::DKIMUtil::headerCanonizationSimple(QStringLiteral("dkim-signature"), dkimValue); break; case MessageViewer::DKIMInfo::CanonicalizationType::Relaxed: mHeaderCanonizationResult += QLatin1String("\r\n") + MessageViewer::DKIMUtil::headerCanonizationRelaxed(QStringLiteral("dkim-signature"), dkimValue, removeQuoteOnContentType); break; } #ifdef DEBUG_SIGNATURE_DKIM QFile headerFile(QStringLiteral("/tmp/headercanon-kmail-%1.txt").arg(removeQuoteOnContentType ? QLatin1String("removequote") : QLatin1String("withquote"))); headerFile.open(QIODevice::WriteOnly | QIODevice::Text); QTextStream outHeaderStream(&headerFile); outHeaderStream << mHeaderCanonizationResult; headerFile.close(); #endif } void DKIMCheckSignatureJob::setHeaderParser(const DKIMHeaderParser &headerParser) { mHeaderParser = headerParser; } void DKIMCheckSignatureJob::setCheckSignatureAuthenticationResult(const QVector &lst) { mCheckSignatureAuthenticationResult = lst; } QString DKIMCheckSignatureJob::bodyCanonizationSimple() const { /* * canonicalize the body using the simple algorithm * specified in Section 3.4.3 of RFC 6376 */ // The "simple" body canonicalization algorithm ignores all empty lines // at the end of the message body. An empty line is a line of zero // length after removal of the line terminator. If there is no body or // no trailing CRLF on the message body, a CRLF is added. It makes no // other changes to the message body. In more formal terms, the // "simple" body canonicalization algorithm converts "*CRLF" at the end // of the body to a single "CRLF". // Note that a completely empty or missing body is canonicalized as a // single "CRLF"; that is, the canonicalized length will be 2 octets. return MessageViewer::DKIMUtil::bodyCanonizationSimple(QString::fromLatin1(mMessage->encodedBody())); } QString DKIMCheckSignatureJob::bodyCanonizationRelaxed() const { /* * canonicalize the body using the relaxed algorithm * specified in Section 3.4.4 of RFC 6376 */ /* a. Reduce whitespace: * Ignore all whitespace at the end of lines. Implementations MUST NOT remove the CRLF at the end of the line. * Reduce all sequences of WSP within a line to a single SP character. b. Ignore all empty lines at the end of the message body. "Empty line" is defined in Section 3.4.3. If the body is non-empty but does not end with a CRLF, a CRLF is added. (For email, this is only possible when using extensions to SMTP or non-SMTP transport mechanisms.) */ const QString returnValue = MessageViewer::DKIMUtil::bodyCanonizationRelaxed(QString::fromLatin1(mMessage->encodedBody())); return returnValue; } QString DKIMCheckSignatureJob::headerCanonizationSimple() const { QString headers; DKIMHeaderParser parser = mHeaderParser; for (const QString &header : mDkimInfo.listSignedHeader()) { const QString str = parser.headerType(header.toLower()); if (!str.isEmpty()) { if (!headers.isEmpty()) { headers += QLatin1String("\r\n"); } headers += MessageViewer::DKIMUtil::headerCanonizationSimple(header, str); } } return headers; } QString DKIMCheckSignatureJob::headerCanonizationRelaxed(bool removeQuoteOnContentType) const { // The "relaxed" header canonicalization algorithm MUST apply the // following steps in order: // o Convert all header field names (not the header field values) to // lowercase. For example, convert "SUBJect: AbC" to "subject: AbC". // o Unfold all header field continuation lines as described in // [RFC5322]; in particular, lines with terminators embedded in // continued header field values (that is, CRLF sequences followed by // WSP) MUST be interpreted without the CRLF. Implementations MUST // NOT remove the CRLF at the end of the header field value. // o Convert all sequences of one or more WSP characters to a single SP // character. WSP characters here include those before and after a // line folding boundary. // o Delete all WSP characters at the end of each unfolded header field // value. // o Delete any WSP characters remaining before and after the colon // separating the header field name from the header field value. The // colon separator MUST be retained. QString headers; DKIMHeaderParser parser = mHeaderParser; for (const QString &header : mDkimInfo.listSignedHeader()) { const QString str = parser.headerType(header.toLower()); if (!str.isEmpty()) { if (!headers.isEmpty()) { headers += QLatin1String("\r\n"); } headers += MessageViewer::DKIMUtil::headerCanonizationRelaxed(header, str, removeQuoteOnContentType); } } return headers; } void DKIMCheckSignatureJob::downloadKey(const DKIMInfo &info) { DKIMDownloadKeyJob *job = new DKIMDownloadKeyJob(this); job->setDomainName(info.domain()); job->setSelectorName(info.selector()); connect(job, &DKIMDownloadKeyJob::error, this, [this](const QString &errorString) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Impossible to start downloadkey: error returned: " << errorString; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::ImpossibleToDownloadKey; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); }); connect(job, &DKIMDownloadKeyJob::success, this, &DKIMCheckSignatureJob::slotDownloadKeyDone); if (!job->start()) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Impossible to start downloadkey"; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::ImpossibleToDownloadKey; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); } } void DKIMCheckSignatureJob::slotDownloadKeyDone(const QList &lst, const QString &domain, const QString &selector) { QByteArray ba; if (lst.count() != 1) { for (const QByteArray &b : lst) { ba += b; } //qCDebug(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Key result has more that 1 element" << lst; } else { ba = lst.at(0); } parseDKIMKeyRecord(QString::fromLocal8Bit(ba), domain, selector, true); } void DKIMCheckSignatureJob::parseDKIMKeyRecord(const QString &str, const QString &domain, const QString &selector, bool storeKeyValue) { qCDebug(MESSAGEVIEWER_DKIMCHECKER_LOG) << "void DKIMCheckSignatureJob::parseDKIMKeyRecord(const QString &str, const QString &domain, const QString &selector, bool storeKeyValue) key:" << str; if (!mDkimKeyRecord.parseKey(str)) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Impossible to parse key record " << str; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; } if (mDkimKeyRecord.keyType() != QLatin1String("rsa")) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "mDkimKeyRecord key type is unknown " << mDkimKeyRecord.keyType() << " str " << str; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; } // if s flag is set in DKIM key record // AUID must be from the same domain as SDID (and not a subdomain) if (mDkimKeyRecord.flags().contains(QLatin1String("s"))) { // s Any DKIM-Signature header fields using the "i=" tag MUST have // the same domain value on the right-hand side of the "@" in the // "i=" tag and the value of the "d=" tag. That is, the "i=" // domain MUST NOT be a subdomain of "d=". Use of this flag is // RECOMMENDED unless subdomaining is required. if (mDkimInfo.iDomain() != mDkimInfo.domain()) { mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::DomainI; Q_EMIT result(createCheckResult()); deleteLater(); return; } } // check that the testing flag is not set if (mDkimKeyRecord.flags().contains(QLatin1String("y"))) { if (!mPolicy.verifySignatureWhenOnlyTest()) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Testing mode!"; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::TestKeyMode; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; } } if (mDkimKeyRecord.publicKey().isEmpty()) { // empty value means that this public key has been revoked qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "mDkimKeyRecord public key is empty. It was revoked "; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::PublicKeyWasRevoked; mStatus = MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; Q_EMIT result(createCheckResult()); deleteLater(); return; } if (storeKeyValue) { Q_EMIT storeKey(str, domain, selector); } verifyRSASignature(); } void DKIMCheckSignatureJob::verifyRSASignature() { QCA::ConvertResult conversionResult; //qDebug() << "mDkimKeyRecord.publicKey() " < currentDate) { mWarning = DKIMCheckSignatureJob::DKIMWarning::SignatureCreatedInFuture; } if (info.signature().isEmpty()) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Signature doesn't exist"; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::MissingSignature; return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; } if (!info.listSignedHeader().contains(QLatin1String("from"), Qt::CaseInsensitive)) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "From is not include in headers list"; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::MissingFrom; return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; } if (info.domain().isEmpty()) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Domain is not defined."; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::DomainNotExist; return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; } if (info.query() != QLatin1String("dns/txt")) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Query is incorrect: " << info.query(); mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InvalidQueryMethod; return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; } if ((info.hashingAlgorithm() == MessageViewer::DKIMInfo::HashingAlgorithmType::Any) || (info.hashingAlgorithm() == MessageViewer::DKIMInfo::HashingAlgorithmType::Unknown)) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "body header algorithm is empty"; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InvalidBodyHashAlgorithm; return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; } if (info.signingAlgorithm().isEmpty()) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "signature algorithm is empty"; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::InvalidSignAlgorithm; return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; } if (info.hashingAlgorithm() == DKIMInfo::HashingAlgorithmType::Sha1) { if (mPolicy.rsaSha1Policy() == MessageViewer::MessageViewerSettings::EnumPolicyRsaSha1::Nothing) { //nothing } else if (mPolicy.rsaSha1Policy() == MessageViewer::MessageViewerSettings::EnumPolicyRsaSha1::Warning) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "hash algorithm is not secure sha1 : Error"; mWarning = MessageViewer::DKIMCheckSignatureJob::DKIMWarning::HashAlgorithmUnsafe; } else if (mPolicy.rsaSha1Policy() == MessageViewer::MessageViewerSettings::EnumPolicyRsaSha1::Error) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "hash algorithm is not secure sha1: Error"; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::HashAlgorithmUnsafeSha1; return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; } } //qDebug() << "info.agentOrUserIdentifier() " << info.agentOrUserIdentifier() << " info.iDomain() " << info.iDomain(); if (!info.agentOrUserIdentifier().endsWith(info.iDomain())) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "AUID is not in a subdomain of SDID"; mError = MessageViewer::DKIMCheckSignatureJob::DKIMError::IDomainError; return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Invalid; } //Add more test //TODO check if info is valid return MessageViewer::DKIMCheckSignatureJob::DKIMStatus::Valid; } DKIMCheckSignatureJob::DKIMError DKIMCheckSignatureJob::error() const { return mError; } DKIMCheckSignatureJob::DKIMStatus DKIMCheckSignatureJob::status() const { return mStatus; } void DKIMCheckSignatureJob::setStatus(DKIMCheckSignatureJob::DKIMStatus status) { mStatus = status; } QString DKIMCheckSignatureJob::dkimValue() const { return mDkimValue; } bool DKIMCheckSignatureJob::CheckSignatureResult::isValid() const { return status != DKIMCheckSignatureJob::DKIMStatus::Unknown; } bool DKIMCheckSignatureJob::CheckSignatureResult::operator==(const DKIMCheckSignatureJob::CheckSignatureResult &other) const { return error == other.error && warning == other.warning && status == other.status && fromEmail == other.fromEmail && auid == other.auid && sdid == other.sdid && listSignatureAuthenticationResult == other.listSignatureAuthenticationResult; } bool DKIMCheckSignatureJob::CheckSignatureResult::operator!=(const DKIMCheckSignatureJob::CheckSignatureResult &other) const { return !CheckSignatureResult::operator==(other); } QDebug operator <<(QDebug d, const DKIMCheckSignatureJob::CheckSignatureResult &t) { d << " error " << t.error; d << " warning " << t.warning; d << " status " << t.status; d << " signedBy " << t.sdid; d << " fromEmail " << t.fromEmail; d << " auid " << t.auid; d << " authenticationResult " << t.listSignatureAuthenticationResult; return d; } QDebug operator <<(QDebug d, const DKIMCheckSignatureJob::DKIMCheckSignatureAuthenticationResult &t) { d << " method " << t.method; d << " errorStr " << t.errorStr; d << " status " << t.status; d << " sdid " << t.sdid; d << " auid " << t.auid; d << " inforesult " << t.infoResult; return d; } bool DKIMCheckSignatureJob::DKIMCheckSignatureAuthenticationResult::operator==(const DKIMCheckSignatureJob::DKIMCheckSignatureAuthenticationResult &other) const { return errorStr == other.errorStr && method == other.method && status == other.status && sdid == other.sdid && auid == other.auid && infoResult == other.infoResult; } bool DKIMCheckSignatureJob::DKIMCheckSignatureAuthenticationResult::isValid() const { //TODO improve it return (method != AuthenticationMethod::Unknown); } diff --git a/messageviewer/src/dkim-verify/dkiminfo.cpp b/messageviewer/src/dkim-verify/dkiminfo.cpp index 824e043d..c91b15a6 100644 --- a/messageviewer/src/dkim-verify/dkiminfo.cpp +++ b/messageviewer/src/dkim-verify/dkiminfo.cpp @@ -1,389 +1,389 @@ /* Copyright (C) 2018-2020 Laurent Montel 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 "dkiminfo.h" #include "dkimutil.h" #include "messageviewer_dkimcheckerdebug.h" #include using namespace MessageViewer; DKIMInfo::DKIMInfo() { } bool DKIMInfo::parseDKIM(const QString &header) { if (header.isEmpty()) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Error: trying to parse empty header"; return false; } QString newHeaders = header; newHeaders.replace(QLatin1String("; "), QLatin1String(";")); #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) const QStringList items = newHeaders.split(QLatin1Char(';'), QString::SkipEmptyParts); #else const QStringList items = newHeaders.split(QLatin1Char(';'), Qt::SkipEmptyParts); #endif bool foundCanonizations = false; for (int i = 0; i < items.count(); ++i) { const QString elem = items.at(i).trimmed(); if (elem.startsWith(QLatin1String("v="))) { - mVersion = elem.right(elem.length() - 2).toInt(); + mVersion = elem.rightRef(elem.length() - 2).toInt(); if (mVersion != 1) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Version is not correct " << mVersion; } } else if (elem.startsWith(QLatin1String("a="))) { //Parse it as "algorithm.signature-algorithm.hash parseAlgorithm(elem.right(elem.length() - 2)); } else if (elem.startsWith(QLatin1String("t="))) { mSignatureTimeStamp = elem.right(elem.length() - 2).toLong(); } else if (elem.startsWith(QLatin1String("c="))) { //Parse header/body canonicalization (example c=relaxed/simple) only relaxed and simple. parseCanonicalization(elem.right(elem.length() - 2)); foundCanonizations = true; } else if (elem.startsWith(QLatin1String("bh="))) { mBodyHash = elem.right(elem.length() - 3).remove(QLatin1Char(' ')); } else if (elem.startsWith(QLatin1String("l="))) { - mBodyLengthCount = elem.right(elem.length() - 2).toInt(); + mBodyLengthCount = elem.rightRef(elem.length() - 2).toInt(); } else if (elem.startsWith(QLatin1String("i="))) { mAgentOrUserIdentifier = elem.right(elem.length() - 2); } else if (elem.startsWith(QLatin1String("q="))) { mQuery = elem.right(elem.length() - 2); if (mQuery != QLatin1String("dns/txt")) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Query is not correct and not supported " << mQuery; } } else if (elem.startsWith(QLatin1String("d="))) { mDomain = elem.right(elem.length() - 2).trimmed(); } else if (elem.startsWith(QLatin1String("s="))) { mSelector = elem.right(elem.length() - 2).trimmed(); } else if (elem.startsWith(QLatin1String("b="))) { mSignature = elem.right(elem.length() - 2); } else if (elem.startsWith(QLatin1String("h="))) { const QString str = MessageViewer::DKIMUtil::cleanString(elem.right(elem.length() - 2)); mListSignedHeader = str.split(QLatin1Char(':')); } else if (elem.startsWith(QLatin1String("x="))) { mExpireTime = elem.right(elem.length() - 2).toLong(); } else if (elem.startsWith(QLatin1String("z="))) { mCopiedHeaderField = elem.right(elem.length() - 2).split(QLatin1Char(':')); } else { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << " Unknown element type" << elem << " : items : " << items; } } if (!foundCanonizations) { //Default mHeaderCanonization = Simple; mBodyCanonization = Simple; } if (mVersion == -1) { mVersion = 1; } if (mQuery.isEmpty()) { mQuery = QLatin1String("dns/txt"); } if (mAgentOrUserIdentifier.isEmpty()) { mAgentOrUserIdentifier = QLatin1Char('@') + mDomain; mIDomain = mDomain; } else { const QStringList lst = mAgentOrUserIdentifier.split(QLatin1Char('@')); if (lst.count() == 2) { if (mAgentOrUserIdentifier.isEmpty()) { mAgentOrUserIdentifier = QLatin1Char('@') + mDomain; } mIDomain = lst.at(1); } } return true; } void DKIMInfo::parseAlgorithm(const QString &str) { // currently only "rsa-sha1" or "rsa-sha256" const QStringList lst = str.split(QLatin1Char('-')); if (lst.count() != 2) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "algorithm is invalid " << str; //Error } else { mSigningAlgorithm = lst.at(0); const QString hashStr = lst.at(1); if (hashStr == QLatin1String("sha1")) { mHashingAlgorithm = HashingAlgorithmType::Sha1; } else if (hashStr == QLatin1String("sha256")) { mHashingAlgorithm = HashingAlgorithmType::Sha256; } else { mHashingAlgorithm = HashingAlgorithmType::Unknown; } } } QString DKIMInfo::iDomain() const { return mIDomain; } void DKIMInfo::setIDomain(const QString &iDomain) { mIDomain = iDomain; } void DKIMInfo::parseCanonicalization(const QString &str) { if (!str.isEmpty()) { const QStringList canonicalizations = str.split(QLatin1Char('/')); //qDebug() << " canonicalizations "<< canonicalizations; if (canonicalizations.count() >= 1) { if (canonicalizations.at(0) == QLatin1String("relaxed")) { mHeaderCanonization = DKIMInfo::Relaxed; } else if (canonicalizations.at(0) == QLatin1String("simple")) { mHeaderCanonization = DKIMInfo::Simple; } else { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "canonicalizations for header unknown " << canonicalizations.at(0); mHeaderCanonization = DKIMInfo::Unknown; return; } if (canonicalizations.count() == 1) { mBodyCanonization = DKIMInfo::Simple; } else if (canonicalizations.count() == 2) { if (canonicalizations.at(1) == QLatin1String("relaxed")) { mBodyCanonization = DKIMInfo::Relaxed; } else if (canonicalizations.at(1) == QLatin1String("simple")) { mBodyCanonization = DKIMInfo::Simple; } else { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "canonicalizations for body unknown " << canonicalizations.at(1); mBodyCanonization = DKIMInfo::Unknown; return; } } else { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << " Problem during parsing canonicalizations " << str; mHeaderCanonization = DKIMInfo::Unknown; mBodyCanonization = DKIMInfo::Unknown; } } } } QStringList DKIMInfo::copiedHeaderField() const { return mCopiedHeaderField; } void DKIMInfo::setCopiedHeaderField(const QStringList &copiedHeaderField) { mCopiedHeaderField = copiedHeaderField; } DKIMInfo::CanonicalizationType DKIMInfo::bodyCanonization() const { return mBodyCanonization; } void DKIMInfo::setBodyCanonization(CanonicalizationType bodyCanonization) { mBodyCanonization = bodyCanonization; } bool DKIMInfo::operator==(const DKIMInfo &other) const { return mVersion == other.version() && mHashingAlgorithm == other.hashingAlgorithm() && mSigningAlgorithm == other.signingAlgorithm() && mDomain == other.domain() && mSelector == other.selector() && mBodyHash == other.bodyHash() && mSignatureTimeStamp == other.signatureTimeStamp() && mExpireTime == other.expireTime() && mQuery == other.query() && mSignature == other.signature() && mAgentOrUserIdentifier == other.agentOrUserIdentifier() && mBodyLengthCount == other.bodyLengthCount() && mListSignedHeader == other.listSignedHeader() && mHeaderCanonization == other.headerCanonization() && mBodyCanonization == other.bodyCanonization() && mIDomain == other.iDomain(); } DKIMInfo::CanonicalizationType DKIMInfo::headerCanonization() const { return mHeaderCanonization; } void DKIMInfo::setHeaderCanonization(CanonicalizationType headerCanonization) { mHeaderCanonization = headerCanonization; } int DKIMInfo::version() const { return mVersion; } void DKIMInfo::setVersion(int version) { mVersion = version; } DKIMInfo::HashingAlgorithmType DKIMInfo::hashingAlgorithm() const { return mHashingAlgorithm; } void DKIMInfo::setHashingAlgorithm(DKIMInfo::HashingAlgorithmType hashingAlgorithm) { mHashingAlgorithm = hashingAlgorithm; } QString DKIMInfo::domain() const { return mDomain; } void DKIMInfo::setDomain(const QString &domain) { mDomain = domain; } QString DKIMInfo::selector() const { return mSelector; } void DKIMInfo::setSelector(const QString &selector) { mSelector = selector; } QString DKIMInfo::bodyHash() const { return mBodyHash; } void DKIMInfo::setBodyHash(const QString &bodyHash) { mBodyHash = bodyHash; } bool DKIMInfo::isValid() const { if (mBodyCanonization == DKIMInfo::Unknown || mHeaderCanonization == DKIMInfo::Unknown) { return false; } return !mSelector.isEmpty() && !mDomain.isEmpty() && !mBodyHash.isEmpty() && ((mHashingAlgorithm == HashingAlgorithmType::Sha1) || mHashingAlgorithm == HashingAlgorithmType::Sha256); } QStringList DKIMInfo::listSignedHeader() const { return mListSignedHeader; } void DKIMInfo::setListSignedHeader(const QStringList &listSignedHeader) { mListSignedHeader = listSignedHeader; } QString DKIMInfo::signingAlgorithm() const { return mSigningAlgorithm; } void DKIMInfo::setSigningAlgorithm(const QString &signingAlgorithm) { mSigningAlgorithm = signingAlgorithm; } qint64 DKIMInfo::signatureTimeStamp() const { return mSignatureTimeStamp; } void DKIMInfo::setSignatureTimeStamp(qint64 signatureTimeStamp) { mSignatureTimeStamp = signatureTimeStamp; } QString DKIMInfo::query() const { return mQuery; } void DKIMInfo::setQuery(const QString &query) { mQuery = query; } qint64 DKIMInfo::expireTime() const { return mExpireTime; } void DKIMInfo::setExpireTime(qint64 expireTime) { mExpireTime = expireTime; } QString DKIMInfo::signature() const { return mSignature; } void DKIMInfo::setSignature(const QString &signature) { mSignature = signature; } QString DKIMInfo::agentOrUserIdentifier() const { return mAgentOrUserIdentifier; } void DKIMInfo::setAgentOrUserIdentifier(const QString &userAgent) { mAgentOrUserIdentifier = userAgent; } int DKIMInfo::bodyLengthCount() const { return mBodyLengthCount; } void DKIMInfo::setBodyLengthCount(int bodyLengthCount) { mBodyLengthCount = bodyLengthCount; } QDebug operator <<(QDebug d, const DKIMInfo &t) { d << "mVersion " << t.version(); d << "mHashingAlgorithm " << t.hashingAlgorithm(); d << "mSigningAlgorithm " << t.signingAlgorithm(); d << "mDomain " << t.domain(); d << "mSelector " << t.selector(); d << "mBodyHash " << t.bodyHash(); d << "mSignatureTimeStamp " << t.signatureTimeStamp(); d << "mExpireTime " << t.expireTime(); d << "mQuery " << t.query(); d << "mSignature " << t.signature(); d << "mAgentOrUserIdentifier " << t.agentOrUserIdentifier(); d << "mBodyLengthCount " << t.bodyLengthCount(); d << "mListSignedHeader " << t.listSignedHeader(); d << "mHeaderCanonization " << t.headerCanonization(); d << "mBodyCanonization " << t.bodyCanonization(); d << "mIdomain " << t.iDomain(); return d; } diff --git a/messageviewer/src/dkim-verify/dmarcinfo.cpp b/messageviewer/src/dkim-verify/dmarcinfo.cpp index 7822c897..9c5f6252 100644 --- a/messageviewer/src/dkim-verify/dmarcinfo.cpp +++ b/messageviewer/src/dkim-verify/dmarcinfo.cpp @@ -1,187 +1,187 @@ /* Copyright (C) 2019-2020 Laurent Montel 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 "dmarcinfo.h" #include "messageviewer_dkimcheckerdebug.h" using namespace MessageViewer; DMARCInfo::DMARCInfo() { } bool DMARCInfo::parseDMARC(const QString &key) { if (key.isEmpty()) { qCWarning(MESSAGEVIEWER_DKIMCHECKER_LOG) << "Error: key empty"; return false; } QString cleanKey = key; cleanKey.replace(QLatin1String("; "), QLatin1String(";")); #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) const QStringList items = cleanKey.split(QLatin1Char(';'), QString::SkipEmptyParts); #else const QStringList items = cleanKey.split(QLatin1Char(';'), Qt::SkipEmptyParts); #endif for (int i = 0; i < items.count(); ++i) { const QString elem = items.at(i).trimmed(); if (elem.startsWith(QLatin1String("v="))) { //v: Version (plain-text; REQUIRED). Identifies the record retrieved // as a DMARC record. It MUST have the value of "DMARC1". The value // of this tag MUST match precisely; if it does not or it is absent, // the entire retrieved record MUST be ignored. It MUST be the first // tag in the list. mVersion = elem.right(elem.length() - 2); } else if (elem.startsWith(QLatin1String("r="))) { //adkim: (plain-text; OPTIONAL; default is "r".) Indicates whether // strict or relaxed DKIM Identifier Alignment mode is required by // the Domain Owner. See Section 3.1.1 for details. Valid values // are as follows: // r: relaxed mode // s: strict mode mAdkim = elem.right(elem.length() - 2); } else if (elem.startsWith(QLatin1String("p="))) { //p: Requested Mail Receiver policy (plain-text; REQUIRED for policy // records). Indicates the policy to be enacted by the Receiver at // the request of the Domain Owner. Policy applies to the domain // queried and to subdomains, unless subdomain policy is explicitly // described using the "sp" tag. This tag is mandatory for policy // records only, but not for third-party reporting records (see // Section 7.1). Possible values are as follows: // none: The Domain Owner requests no specific action be taken // regarding delivery of messages. // quarantine: The Domain Owner wishes to have email that fails the // DMARC mechanism check be treated by Mail Receivers as // suspicious. Depending on the capabilities of the Mail // Receiver, this can mean "place into spam folder", "scrutinize // with additional intensity", and/or "flag as suspicious". // reject: The Domain Owner wishes for Mail Receivers to reject // email that fails the DMARC mechanism check. Rejection SHOULD // occur during the SMTP transaction. See Section 10.3 for some // discussion of SMTP rejection methods and their implications. mPolicy = elem.right(elem.length() - 2); } else if (elem.startsWith(QLatin1String("ptc="))) { //pct: (plain-text integer between 0 and 100, inclusive; OPTIONAL; // default is 100). Percentage of messages from the Domain Owner's // mail stream to which the DMARC policy is to be applied. However, // this MUST NOT be applied to the DMARC-generated reports, all of // which must be sent and received unhindered. The purpose of the // "pct" tag is to allow Domain Owners to enact a slow rollout // enforcement of the DMARC mechanism. The prospect of "all or // nothing" is recognized as preventing many organizations from // experimenting with strong authentication-based mechanisms. See // Section 6.6.4 for details. Note that random selection based on // this percentage, such as the following pseudocode, is adequate: // if (random mod 100) < pct then // selected = true // else // selected = false //TODO verify if it's a percentage - mPercentage = elem.right(elem.length() - 4).toInt(); + mPercentage = elem.rightRef(elem.length() - 4).toInt(); } else if (elem.startsWith(QLatin1String("sp="))) { //sp: Requested Mail Receiver policy for all subdomains (plain-text; // OPTIONAL). Indicates the policy to be enacted by the Receiver at // the request of the Domain Owner. It applies only to subdomains of // the domain queried and not to the domain itself. Its syntax is // identical to that of the "p" tag defined above. If absent, the // policy specified by the "p" tag MUST be applied for subdomains. // Note that "sp" will be ignored for DMARC records published on // subdomains of Organizational Domains due to the effect of the // DMARC policy discovery mechanism described in Section 6.6.3. mSubDomainPolicy = elem.right(elem.length() - 3); } } if (mAdkim.isEmpty() && mVersion == QLatin1String("DMARC1")) { mAdkim = QLatin1Char('r'); } return true; } QString DMARCInfo::version() const { return mVersion; } void DMARCInfo::setVersion(const QString &version) { mVersion = version; } QString DMARCInfo::adkim() const { return mAdkim; } void DMARCInfo::setAdkim(const QString &adkim) { mAdkim = adkim; } QString DMARCInfo::policy() const { return mPolicy; } void DMARCInfo::setPolicy(const QString &policy) { mPolicy = policy; } int DMARCInfo::percentage() const { return mPercentage; } void DMARCInfo::setPercentage(int percentage) { mPercentage = percentage; } QString DMARCInfo::subDomainPolicy() const { return mSubDomainPolicy; } void DMARCInfo::setSubDomainPolicy(const QString &subDomainPolicy) { mSubDomainPolicy = subDomainPolicy; } bool DMARCInfo::operator==(const DMARCInfo &other) const { return mVersion == other.version() && mAdkim == other.adkim() && mPolicy == other.policy() && mSubDomainPolicy == other.subDomainPolicy() && mPercentage == other.percentage(); } QDebug operator <<(QDebug d, const DMARCInfo &t) { d << " mVersion " << t.version(); d << " mAdkim " << t.adkim(); d << " mPolicy " << t.policy(); d << " mSubDomainPolicy " << t.subDomainPolicy(); d << " mPercentage " << t.percentage(); return d; } diff --git a/messageviewer/src/messagepartthemes/default/messagepartrendererfactory.cpp b/messageviewer/src/messagepartthemes/default/messagepartrendererfactory.cpp index fb571860..d86f47c8 100644 --- a/messageviewer/src/messagepartthemes/default/messagepartrendererfactory.cpp +++ b/messageviewer/src/messagepartthemes/default/messagepartrendererfactory.cpp @@ -1,188 +1,189 @@ /* This file is part of KMail, the KDE mail client. Copyright (c) 2017 Sandro Knauß KMail 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. KMail 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 "messagepartrendererfactory.h" #include "messagepartrendererfactory_p.h" #include "messagepartrenderplugin.h" #include "viewer/urlhandlermanager.h" #include "messagepartrendererbase.h" #include "messageviewer_debug.h" #include "plugins/attachmentmessagepartrenderer.h" #include "plugins/messagepartrenderer.h" #include "plugins/textmessagepartrenderer.h" #include #include #include #include using namespace MessageViewer; void MessagePartRendererFactoryPrivate::setup() { if (m_renderers.isEmpty()) { initialize_builtin_renderers(); loadPlugins(); } Q_ASSERT(!m_renderers.isEmpty()); } void MessagePartRendererFactoryPrivate::loadPlugins() { if (m_pluginSubdir.isEmpty()) { return; } KPluginLoader::forEachPlugin(m_pluginSubdir, [this](const QString &path) { QPluginLoader loader(path); const auto pluginData = loader.metaData().value(QLatin1String("MetaData")).toObject().value(QLatin1String("renderer")).toArray(); if (pluginData.isEmpty()) { qCWarning(MESSAGEVIEWER_LOG) << "Plugin" << path << "has no meta data."; return; } auto plugin = qobject_cast(loader.instance()); if (!plugin) { qCWarning(MESSAGEVIEWER_LOG) << path << "is not a MessagePartRendererPlugin"; return; } MessagePartRendererBase *renderer = nullptr; for (int i = 0; (renderer = plugin->renderer(i)) && i < pluginData.size(); ++i) { const auto metaData = pluginData.at(i).toObject(); const auto type = metaData.value(QLatin1String("type")).toString().toUtf8(); if (type.isEmpty()) { qCWarning(MESSAGEVIEWER_LOG) << path << "returned empty type specification for index" << i; break; } const auto mimetype = metaData.value(QLatin1String("mimetype")).toString().toLower(); // priority should always be higher than the built-in ones, otherwise what's the point? const auto priority = metaData.value(QLatin1String("priority")).toInt() + 100; qCDebug(MESSAGEVIEWER_LOG) << "renderer plugin for " << type << mimetype << priority; insert(type, renderer, mimetype, priority); } const Interface::BodyPartURLHandler *handler = nullptr; for (int i = 0; (handler = plugin->urlHandler(i)); ++i) { const auto metaData = pluginData.at(i).toObject(); const auto mimeType = metaData.value(QLatin1String("mimetype")).toString().toLower(); URLHandlerManager::instance()->registerHandler(handler, mimeType); } }); } void MessagePartRendererFactoryPrivate::initialize_builtin_renderers() { insert("MimeTreeParser::MessagePart", new MessagePartRenderer()); insert("MimeTreeParser::TextMessagePart", new TextMessagePartRenderer()); insert("MimeTreeParser::AttachmentMessagePart", new AttachmentMessagePartRenderer()); } void MessagePartRendererFactoryPrivate::insert(const QByteArray &type, MessagePartRendererBase *renderer, const QString &mimeType, int priority) { if (type.isEmpty() || !renderer) { return; } QMimeDatabase db; const auto mt = db.mimeTypeForName(mimeType); RendererInfo info; info.renderer.reset(renderer); info.mimeType = mt.isValid() ? mt.name() : mimeType; info.priority = priority; auto &v = m_renderers[type]; v.push_back(info); } MessagePartRendererFactory::MessagePartRendererFactory() : d(new MessagePartRendererFactoryPrivate) { } MessagePartRendererFactory::~MessagePartRendererFactory() = default; void MessagePartRendererFactory::setPluginPath(const QString &subdir) { d->m_pluginSubdir = subdir; } MessagePartRendererFactory *MessagePartRendererFactory::instance() { static MessagePartRendererFactory s_instance; return &s_instance; } QVector MessagePartRendererFactory::renderersForPart(const QMetaObject *mo, const MimeTreeParser::MessagePartPtr &mp) const { d->setup(); const auto mtName = mp->content() ? QString::fromUtf8(mp->content()->contentType()->mimeType().toLower()) : QString(); QMimeDatabase db; const auto mt = db.mimeTypeForName(mtName); auto ancestors = mt.allAncestors(); if (mt.isValid() || !mtName.isEmpty()) { ancestors.prepend(mt.isValid() ? mt.name() : mtName); } auto candidates = d->m_renderers.value(mo->className()); // remove candidates with a mimetype set that don't match the mimetype of the part candidates.erase(std::remove_if(candidates.begin(), candidates.end(), [ancestors](const RendererInfo &info) { if (info.mimeType.isEmpty()) { return false; } return !ancestors.contains(info.mimeType); }), candidates.end()); // sort most specific mimetpypes first std::stable_sort(candidates.begin(), candidates.end(), [ancestors](const RendererInfo &lhs, const RendererInfo &rhs) { if (lhs.mimeType == rhs.mimeType) { return lhs.priority > rhs.priority; } if (lhs.mimeType.isEmpty()) { return false; } if (rhs.mimeType.isEmpty()) { return true; } return ancestors.indexOf(lhs.mimeType) < ancestors.indexOf(rhs.mimeType); }); QVector r; + r.reserve(candidates.size()); for (const auto &candidate : candidates) { r.push_back(candidate.renderer.data()); } return r; } diff --git a/webengineviewer/src/checkphishingurl/createdatabasefilejob.cpp b/webengineviewer/src/checkphishingurl/createdatabasefilejob.cpp index 2c94a58e..cddb20f1 100644 --- a/webengineviewer/src/checkphishingurl/createdatabasefilejob.cpp +++ b/webengineviewer/src/checkphishingurl/createdatabasefilejob.cpp @@ -1,248 +1,248 @@ /* Copyright (C) 2016-2020 Laurent Montel 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 "createdatabasefilejob.h" #include "checkphishingurlutil.h" #include "webengineviewer_debug.h" #include "localdatabasefile.h" #include "riceencodingdecoder.h" #include #include using namespace WebEngineViewer; class WebEngineViewer::CreateDatabaseFileJobPrivate { public: CreateDatabaseFileJobPrivate(CreateDatabaseFileJob *qq) : q(qq) { } void createFileFromFullUpdate(const QVector &additionList); void removeElementFromDataBase(const QVector &removalList, QVector &oldDataBaseAddition); void createBinaryFile(); void generateFile(bool fullUpdate); WebEngineViewer::UpdateDataBaseInfo mInfoDataBase; QString mFileName; QFile mFile; CreateDatabaseFileJob *q; }; void CreateDatabaseFileJobPrivate::createFileFromFullUpdate(const QVector &additionList) { //1 add version number const quint16 major = WebEngineViewer::CheckPhishingUrlUtil::majorVersion(); const quint16 minor = WebEngineViewer::CheckPhishingUrlUtil::minorVersion(); qint64 hashStartPosition = mFile.write(reinterpret_cast(&major), sizeof(major)); hashStartPosition += mFile.write(reinterpret_cast(&minor), sizeof(minor)); //2 add number of items - QList itemToStore; + QVector itemToStore; for (const Addition &add : additionList) { switch (add.compressionType) { case UpdateDataBaseInfo::RawCompression: { //qCWarning(WEBENGINEVIEWER_LOG) << " add.size" << add.prefixSize; const QByteArray uncompressed = add.hashString; for (int i = 0; i < uncompressed.size();) { const QByteArray m = uncompressed.mid(i, add.prefixSize); i += add.prefixSize; Addition tmp; tmp.hashString = m; tmp.prefixSize = add.prefixSize; itemToStore << tmp; //We store index as 8 octets. hashStartPosition += 8; if (m.size() != add.prefixSize) { qCWarning(WEBENGINEVIEWER_LOG) << "hash string: " << m << " hash string size: " << m.size(); } } break; } case UpdateDataBaseInfo::RiceCompression: { //TODO qCWarning(WEBENGINEVIEWER_LOG) << "Rice compression still not implemented"; const QVector listRice = WebEngineViewer::RiceEncodingDecoder::decodeRiceHashesDelta(add.riceDeltaEncoding); qDebug() << " listRice" << listRice; break; } case UpdateDataBaseInfo::UnknownCompression: qCWarning(WEBENGINEVIEWER_LOG) << "Unknown compression type in addition element"; break; } } const quint64 numberOfElement = itemToStore.count(); hashStartPosition += mFile.write(reinterpret_cast(&numberOfElement), sizeof(numberOfElement)); //3 add index of items //Order it first std::sort(itemToStore.begin(), itemToStore.end(), Addition::lessThan); quint64 tmpPos = hashStartPosition; for (const Addition &add : qAsConst(itemToStore)) { mFile.write(reinterpret_cast(&tmpPos), sizeof(tmpPos)); tmpPos += add.prefixSize + 1; //We add +1 as we store '\0' } //4 add items QByteArray newSsha256; for (const Addition &add : qAsConst(itemToStore)) { const QByteArray storedBa = add.hashString + '\0'; mFile.write(reinterpret_cast(storedBa.constData()), storedBa.size()); newSsha256 += add.hashString; } mFile.close(); //Verify hash with sha256 const QByteArray newSsha256Value = QCryptographicHash::hash(newSsha256, QCryptographicHash::Sha256); const bool checkSumCorrect = (mInfoDataBase.sha256 == newSsha256Value.toBase64()); if (!checkSumCorrect) { qCWarning(WEBENGINEVIEWER_LOG) << " newSsha256Value different from sha256 : " << newSsha256Value.toBase64() << " from server " << mInfoDataBase.sha256; } Q_EMIT q->finished(checkSumCorrect, mInfoDataBase.newClientState, mInfoDataBase.minimumWaitDuration); } void CreateDatabaseFileJobPrivate::generateFile(bool fullUpdate) { qCDebug(WEBENGINEVIEWER_LOG) << " void CreateDatabaseFileJobPrivate::generateFile(bool fullUpdate)" << fullUpdate; mFile.setFileName(mFileName); if (fullUpdate) { if (mFile.exists() && !mFile.remove()) { qCWarning(WEBENGINEVIEWER_LOG) << "Impossible to remove database file " << mFileName; Q_EMIT q->finished(false, QString(), QString()); return; } if (!mFile.open(QIODevice::WriteOnly)) { qCWarning(WEBENGINEVIEWER_LOG) << "Impossible to open database file " << mFileName; Q_EMIT q->finished(false, QString(), QString()); return; } createFileFromFullUpdate(mInfoDataBase.additionList); } else { WebEngineViewer::LocalDataBaseFile localeFile(mFileName); if (!localeFile.fileExists()) { qCWarning(WEBENGINEVIEWER_LOG) << "Impossible to create partial update as file doesn't exist"; Q_EMIT q->finished(false, QString(), QString()); return; } //Read Element from database. QVector oldDataBaseAddition = localeFile.extractAllInfo(); removeElementFromDataBase(mInfoDataBase.removalList, oldDataBaseAddition); QVector additionList = mInfoDataBase.additionList; // Add value found in database oldDataBaseAddition += additionList; //Close file localeFile.close(); if (!mFile.remove()) { qCWarning(WEBENGINEVIEWER_LOG) << "Impossible to remove database file " << mFileName; Q_EMIT q->finished(false, QString(), QString()); return; } if (!mFile.open(QIODevice::WriteOnly)) { qCWarning(WEBENGINEVIEWER_LOG) << "Impossible to open database file " << mFileName; Q_EMIT q->finished(false, QString(), QString()); return; } createFileFromFullUpdate(oldDataBaseAddition); } } void CreateDatabaseFileJobPrivate::removeElementFromDataBase(const QVector &removalList, QVector &oldDataBaseAddition) { QVector indexToRemove; for (const Removal &removeItem : removalList) { switch (removeItem.compressionType) { case UpdateDataBaseInfo::RawCompression: for (int id : qAsConst(removeItem.indexes)) { indexToRemove << id; } break; case UpdateDataBaseInfo::RiceCompression: indexToRemove = WebEngineViewer::RiceEncodingDecoder::decodeRiceIndiceDelta(removeItem.riceDeltaEncoding); break; case UpdateDataBaseInfo::UnknownCompression: qCWarning(WEBENGINEVIEWER_LOG) << " Unknown compression type defined in removal elements. It's a bug!"; break; } } std::sort(indexToRemove.begin(), indexToRemove.end()); for (int i = (indexToRemove.count() - 1); i >= 0; --i) { oldDataBaseAddition.remove(indexToRemove.at(i)); } } void CreateDatabaseFileJobPrivate::createBinaryFile() { switch (mInfoDataBase.responseType) { case UpdateDataBaseInfo::Unknown: qCWarning(WEBENGINEVIEWER_LOG) << " Response Type of database info is \"unknown\". It's a bug!"; break; case UpdateDataBaseInfo::FullUpdate: case UpdateDataBaseInfo::PartialUpdate: generateFile((mInfoDataBase.responseType == UpdateDataBaseInfo::FullUpdate)); break; } q->deleteLater(); } CreateDatabaseFileJob::CreateDatabaseFileJob(QObject *parent) : QObject(parent) , d(new WebEngineViewer::CreateDatabaseFileJobPrivate(this)) { } CreateDatabaseFileJob::~CreateDatabaseFileJob() { delete d; } bool CreateDatabaseFileJob::canStart() const { return !d->mFileName.isEmpty() && d->mInfoDataBase.isValid(); } void CreateDatabaseFileJob::setUpdateDataBaseInfo(const UpdateDataBaseInfo &infoDataBase) { d->mInfoDataBase = infoDataBase; } void CreateDatabaseFileJob::start() { if (!canStart()) { Q_EMIT finished(false, QString(), QString()); deleteLater(); } else { d->createBinaryFile(); } } void CreateDatabaseFileJob::setFileName(const QString &filename) { d->mFileName = filename; } diff --git a/webengineviewer/src/webengineaccesskey/webengineaccesskey.cpp b/webengineviewer/src/webengineaccesskey/webengineaccesskey.cpp index 34d0e73e..dbd34296 100644 --- a/webengineviewer/src/webengineaccesskey/webengineaccesskey.cpp +++ b/webengineviewer/src/webengineaccesskey/webengineaccesskey.cpp @@ -1,367 +1,368 @@ /* Copyright (C) 2016-2020 Laurent Montel 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 "webengineaccesskey.h" #include "webengineaccesskeyanchor.h" #include "webengineaccesskeyutils.h" #include "webenginemanagescript.h" #include #include #include #include #include #include #include #include using namespace WebEngineViewer; template struct InvokeWrapper { R *receiver; void (C::*memberFunction)(Arg); void operator()(Arg result) { (receiver->*memberFunction)(result); } }; template InvokeWrapper invoke(R *receiver, void (C::*memberFunction)(Arg)) { InvokeWrapper wrapper = {receiver, memberFunction}; return wrapper; } class WebEngineViewer::WebEngineAccessKeyPrivate { public: enum AccessKeyState { NotActivated, PreActivated, Activated }; WebEngineAccessKeyPrivate(WebEngineAccessKey *qq, QWebEngineView *webEngine) : mWebEngine(webEngine) , q(qq) { } void makeAccessKeyLabel(QChar accessKey, const WebEngineViewer::WebEngineAccessKeyAnchor &element); bool checkForAccessKey(QKeyEvent *event); QVector mAccessKeyLabels; QMultiHash mAccessKeyNodes; QHash mDuplicateLinkElements; QWebEngineView *mWebEngine = nullptr; AccessKeyState mAccessKeyActivated = NotActivated; KActionCollection *mActionCollection = nullptr; WebEngineAccessKey *q = nullptr; }; static QString linkElementKey(const WebEngineViewer::WebEngineAccessKeyAnchor &element, const QUrl &baseUrl) { //qDebug()<<" element.href()"< *dupLinkList, QChar *accessKey, const QUrl &baseUrl) { if (element.tagName().compare(QLatin1String("A"), Qt::CaseInsensitive) == 0) { const QString linkKey(linkElementKey(element, baseUrl)); //qDebug() << "LINK KEY:" << linkKey; if (dupLinkList->contains(linkKey)) { //qDebug() << "***** Found duplicate link element:" << linkKey; *accessKey = dupLinkList->value(linkKey); } else if (!linkKey.isEmpty()) { dupLinkList->insert(linkKey, *accessKey); } if (linkKey.isEmpty()) { *accessKey = QChar(); } } } static bool isHiddenElement(const WebEngineViewer::WebEngineAccessKeyAnchor &element) { // width or height property set to less than zero if (element.boundingRect().width() < 1 || element.boundingRect().height() < 1) { return true; } #if 0 // visibility set to 'hidden' in the element itself or its parent elements. if (element.styleProperty(QStringLiteral("visibility"), QWebElement::ComputedStyle).compare(QLatin1String("hidden"), Qt::CaseInsensitive) == 0) { return true; } // display set to 'none' in the element itself or its parent elements. if (element.styleProperty(QStringLiteral("display"), QWebElement::ComputedStyle).compare(QLatin1String("none"), Qt::CaseInsensitive) == 0) { return true; } #endif return false; } bool WebEngineAccessKeyPrivate::checkForAccessKey(QKeyEvent *event) { if (mAccessKeyLabels.isEmpty()) { return false; } QString text = event->text(); if (text.isEmpty()) { return false; } QChar key = text.at(0).toUpper(); bool handled = false; if (mAccessKeyNodes.contains(key)) { WebEngineViewer::WebEngineAccessKeyAnchor element = mAccessKeyNodes.value(key); if (element.tagName().compare(QLatin1String("A"), Qt::CaseInsensitive) == 0) { const QString linkKey(linkElementKey(element, mWebEngine->url())); if (!linkKey.isEmpty()) { //qDebug()<<" WebEngineAccessKey::checkForAccessKey****"<openUrl(QUrl(linkKey)); handled = true; } } } return handled; } void WebEngineAccessKeyPrivate::makeAccessKeyLabel(QChar accessKey, const WebEngineViewer::WebEngineAccessKeyAnchor &element) { //qDebug()<<" void WebEngineAccessKey::makeAccessKeyLabel(QChar accessKey, const WebEngineViewer::MailWebEngineAccessKeyAnchor &element)"; QLabel *label = new QLabel(mWebEngine); QFont font(label->font()); font.setBold(true); label->setFont(font); label->setText(accessKey); QFontMetrics metric(label->font()); label->setFixedWidth(metric.boundingRect(QStringLiteral("WW")).width()); label->setPalette(QToolTip::palette()); label->setAutoFillBackground(true); label->setFrameStyle(QFrame::Box | QFrame::Plain); QPoint point = element.boundingRect().center(); label->move(point); label->show(); point.setX(point.x() - label->width() / 2); label->move(point); mAccessKeyLabels.append(label); mAccessKeyNodes.insert(accessKey, element); } WebEngineAccessKey::WebEngineAccessKey(QWebEngineView *webEngine, QObject *parent) : QObject(parent) , d(new WebEngineViewer::WebEngineAccessKeyPrivate(this, webEngine)) { //qDebug() << " WebEngineAccessKey::WebEngineAccessKey(QWebEngineView *webEngine, QObject *parent)"; } WebEngineAccessKey::~WebEngineAccessKey() { delete d; } void WebEngineAccessKey::setActionCollection(KActionCollection *ac) { d->mActionCollection = ac; } void WebEngineAccessKey::wheelEvent(QWheelEvent *e) { hideAccessKeys(); if (d->mAccessKeyActivated == WebEngineAccessKeyPrivate::PreActivated && (e->modifiers() & Qt::ControlModifier)) { d->mAccessKeyActivated = WebEngineAccessKeyPrivate::NotActivated; } } void WebEngineAccessKey::resizeEvent(QResizeEvent *) { if (d->mAccessKeyActivated == WebEngineAccessKeyPrivate::Activated) { hideAccessKeys(); } } void WebEngineAccessKey::keyPressEvent(QKeyEvent *e) { if (e && d->mWebEngine->hasFocus()) { if (d->mAccessKeyActivated == WebEngineAccessKeyPrivate::Activated) { if (d->checkForAccessKey(e)) { hideAccessKeys(); e->accept(); return; } hideAccessKeys(); } else if (e->key() == Qt::Key_Control && e->modifiers() == Qt::ControlModifier #if 0 //FIXME && !isEditableElement(d->mWebView->page()) #endif ) { d->mAccessKeyActivated = WebEngineAccessKeyPrivate::PreActivated; // Only preactive here, it will be actually activated in key release. } } } void WebEngineAccessKey::keyReleaseEvent(QKeyEvent *e) { //qDebug() << " void WebEngineAccessKey::keyReleaseEvent(QKeyEvent *e)"; if (d->mAccessKeyActivated == WebEngineAccessKeyPrivate::PreActivated) { // Activate only when the CTRL key is pressed and released by itself. if (e->key() == Qt::Key_Control && e->modifiers() == Qt::NoModifier) { showAccessKeys(); } else { d->mAccessKeyActivated = WebEngineAccessKeyPrivate::NotActivated; } } } void WebEngineAccessKey::hideAccessKeys() { if (!d->mAccessKeyLabels.isEmpty()) { for (int i = 0, count = d->mAccessKeyLabels.count(); i < count; ++i) { QLabel *label = d->mAccessKeyLabels[i]; label->hide(); label->deleteLater(); } d->mAccessKeyLabels.clear(); d->mAccessKeyNodes.clear(); d->mDuplicateLinkElements.clear(); d->mAccessKeyActivated = WebEngineAccessKeyPrivate::NotActivated; d->mWebEngine->update(); } } void WebEngineAccessKey::handleSearchAccessKey(const QVariant &res) { //qDebug() << " void WebEngineAccessKey::handleSearchAccessKey(const QVariant &res)" << res; const QVariantList lst = res.toList(); QVector anchorList; anchorList.reserve(lst.count()); for (const QVariant &var : lst) { //qDebug()<<" var"< unusedKeys; unusedKeys.reserve(10 + ('Z' - 'A' + 1)); for (char c = 'A'; c <= 'Z'; ++c) { unusedKeys << QLatin1Char(c); } for (char c = '0'; c <= '9'; ++c) { unusedKeys << QLatin1Char(c); } if (d->mActionCollection) { - for (QAction *act : d->mActionCollection->actions()) { + const auto actions = d->mActionCollection->actions(); + for (QAction *act : actions) { if (act) { const QKeySequence shortCut = act->shortcut(); if (!shortCut.isEmpty()) { auto lstUnusedKeys = unusedKeys; for (QChar c : qAsConst(unusedKeys)) { if (shortCut.matches(QKeySequence(c)) != QKeySequence::NoMatch) { lstUnusedKeys.removeOne(c); } } unusedKeys = lstUnusedKeys; } } } } QVector unLabeledElements; QRect viewport = d->mWebEngine->rect(); for (const WebEngineViewer::WebEngineAccessKeyAnchor &element : qAsConst(anchorList)) { const QRect geometry = element.boundingRect(); if (geometry.size().isEmpty() || !viewport.contains(geometry.topLeft())) { continue; } if (isHiddenElement(element)) { continue; // Do not show access key for hidden elements... } const QString accessKeyAttribute(element.accessKey().toUpper()); if (accessKeyAttribute.isEmpty()) { unLabeledElements.append(element); continue; } QChar accessKey; for (int i = 0; i < accessKeyAttribute.count(); i += 2) { const QChar &possibleAccessKey = accessKeyAttribute[i]; if (unusedKeys.contains(possibleAccessKey)) { accessKey = possibleAccessKey; break; } } if (accessKey.isNull()) { unLabeledElements.append(element); continue; } handleDuplicateLinkElements(element, &d->mDuplicateLinkElements, &accessKey, d->mWebEngine->url()); if (!accessKey.isNull()) { unusedKeys.removeOne(accessKey); d->makeAccessKeyLabel(accessKey, element); } } // Pick an access key first from the letters in the text and then from the // list of unused access keys for (const WebEngineViewer::WebEngineAccessKeyAnchor &element : qAsConst(unLabeledElements)) { const QRect geometry = element.boundingRect(); if (unusedKeys.isEmpty() || geometry.size().isEmpty() || !viewport.contains(geometry.topLeft())) { continue; } QChar accessKey; const QString text = element.innerText().toUpper(); for (int i = 0; i < text.count(); ++i) { const QChar &c = text.at(i); if (unusedKeys.contains(c)) { accessKey = c; break; } } if (accessKey.isNull()) { accessKey = unusedKeys.takeFirst(); } handleDuplicateLinkElements(element, &d->mDuplicateLinkElements, &accessKey, d->mWebEngine->url()); if (!accessKey.isNull()) { unusedKeys.removeOne(accessKey); d->makeAccessKeyLabel(accessKey, element); } } d->mAccessKeyActivated = (!d->mAccessKeyLabels.isEmpty() ? WebEngineAccessKeyPrivate::Activated : WebEngineAccessKeyPrivate::NotActivated); } void WebEngineAccessKey::showAccessKeys() { d->mAccessKeyActivated = WebEngineAccessKeyPrivate::Activated; d->mWebEngine->page()->runJavaScript(WebEngineViewer::WebEngineAccessKeyUtils::script(), WebEngineManageScript::scriptWordId(), invoke(this, &WebEngineAccessKey::handleSearchAccessKey)); }