diff --git a/CMakeLists.txt b/CMakeLists.txt index a7f6e27..2d08172 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,60 +1,71 @@ # SPDX-FileCopyrightText: 2023 g10 code Gmbh # SPDX-Contributor: Carl Schwan # SPDX-License-Identifier: BSD-2-Clause cmake_minimum_required(VERSION 3.20) project(gpgoljs) set(KF_MIN_VERSION "6.0.0") set(QT_MIN_VERSION "6.6.0") set(MIMETREEPARSER_VERSION "6.0.0") set(LIBKLEO_VERSION "6.0.0") set(LIBKDEPIM_VERSION "6.0.0") set(PIMTEXTEDIT_VERSION "6.0.0") find_package(ECM ${KF_MIN_VERSION} CONFIG REQUIRED) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH} ) find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake) include(FeatureSummary) include(KDEInstallDirs) include(KDECMakeSettings) +include(KDECompilerSettings NO_POLICY_SCOPE) + include(ECMQtDeclareLoggingCategory) include(ECMAddTests) -include(KDECompilerSettings NO_POLICY_SCOPE) +include(KDEGitCommitHooks) +include(KDECMakeSettings) +include(KDEClangFormat) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Core HttpServer Widgets PrintSupport WebSockets) set_package_properties(Qt6 PROPERTIES TYPE REQUIRED PURPOSE "Basic application components" ) find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Contacts Completion CoreAddons WidgetsAddons Config ColorScheme Codecs XmlGui GuiAddons JobWidgets Sonnet CalendarCore Archive) set_package_properties(KF6 PROPERTIES TYPE REQUIRED PURPOSE "Basic application components" ) find_package(KPim6Libkleo ${LIBKLEO_VERSION} CONFIG REQUIRED) find_package(KPim6Libkdepim ${LIBKDEPIM_LIB_VERSION} CONFIG REQUIRED) find_package(KPim6MimeTreeParserWidgets ${MIMETREEPARSER_VERSION} CONFIG REQUIRED) find_package(KPim6TextEdit ${PIMTEXTEDIT_VERSION} CONFIG REQUIRED) find_package(KF6TextAutoCorrectionCore CONFIG REQUIRED) if (BUILD_TESTING) find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test) endif() add_subdirectory(common) add_subdirectory(server) add_subdirectory(client) + +feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) + +file(GLOB_RECURSE ALL_CLANG_FORMAT_SOURCE_FILES client/*.cpp client/*.h server/*.cpp server/*.h) +kde_clang_format(${ALL_CLANG_FORMAT_SOURCE_FILES}) + +kde_configure_git_pre_commit_hook(CHECKS CLANG_FORMAT) diff --git a/client/autotests/attachmentjobtest.cpp b/client/autotests/attachmentjobtest.cpp index 619d266..777d8be 100644 --- a/client/autotests/attachmentjobtest.cpp +++ b/client/autotests/attachmentjobtest.cpp @@ -1,105 +1,105 @@ /* SPDX-FileCopyrightText: 2009 Constantin Berzan SPDX-License-Identifier: LGPL-2.0-or-later */ #include "attachmentjobtest.h" #include "qtest_messagecomposer.h" #include #include #include #include -#include "../editor/job/attachmentjob.h" +#include "../editor/attachment/attachmentpart.h" #include "../editor/composer.h" +#include "../editor/job/attachmentjob.h" #include "../editor/part/globalpart.h" -#include "../editor/attachment/attachmentpart.h" using namespace MessageComposer; using namespace KMime; using namespace MessageCore; #define PATH_ATTACHMENTS QLatin1String(KDESRCDIR "/attachments/") QTEST_MAIN(AttachmentJobTest) void AttachmentJobTest::testAttachment() { const QString name = QStringLiteral("name"); const QString fileName = QStringLiteral("filename"); const QString description = QStringLiteral("long long long description..."); const QByteArray mimeType("x-some/x-type"); const QByteArray data("la la la"); AttachmentPart::Ptr part = AttachmentPart::Ptr(new AttachmentPart); part->setName(name); part->setFileName(fileName); part->setDescription(description); part->setMimeType(mimeType); part->setData(data); Composer composer; composer.globalPart()->setFallbackCharsetEnabled(true); auto ajob = new AttachmentJob(part, &composer); QVERIFY(ajob->exec()); Content *result = ajob->content(); result->assemble(); qDebug() << result->encodedContent(); QCOMPARE(result->contentType(false)->name(), name); QCOMPARE(result->contentDisposition(false)->filename(), fileName); QCOMPARE(result->contentDescription(false)->asUnicodeString(), description); QCOMPARE(result->contentType(false)->mimeType(), mimeType); QCOMPARE(result->body(), data); QVERIFY(result->contentDisposition(false)->disposition() == Headers::CDattachment); delete ajob; ajob = nullptr; // delete result; } #if 0 // Disabled: using UTF-8 instead of trying to detect charset. void AttachmentJobTest::testTextCharsetAutodetect_data() { QTest::addColumn("url"); QTest::addColumn("charset"); // PATH_ATTACHMENTS is defined by CMake. QTest::newRow("ascii") << QUrl::fromLocalFile(PATH_ATTACHMENTS + QString::fromLatin1("ascii.txt")) << QByteArray("us-ascii"); QTest::newRow("iso8859-2") << QUrl::fromLocalFile(PATH_ATTACHMENTS + QString::fromLatin1("iso8859-2.txt")) << QByteArray("iso-8859-2"); // TODO not sure how to test utf-16. } void AttachmentJobTest::testTextCharsetAutodetect() { QFETCH(QUrl, url); QFETCH(QByteArray, charset); AttachmentFromUrlJob *ljob = new AttachmentFromUrlJob(url); VERIFYEXEC(ljob); AttachmentPart::Ptr part = ljob->attachmentPart(); delete ljob; ljob = 0; Composer *composer = new Composer; composer->globalPart()->setFallbackCharsetEnabled(true); AttachmentJob *ajob = new AttachmentJob(part, composer); VERIFYEXEC(ajob); Content *result = ajob->content(); delete ajob; ajob = 0; result->assemble(); qDebug() << result->encodedContent(); QCOMPARE(result->contentType(false)->charset(), charset); } #endif #include "moc_attachmentjobtest.cpp" diff --git a/client/autotests/emailcontrollertest.cpp b/client/autotests/emailcontrollertest.cpp index 247060f..8a11031 100644 --- a/client/autotests/emailcontrollertest.cpp +++ b/client/autotests/emailcontrollertest.cpp @@ -1,152 +1,150 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later -#include -#include -#include -#include -#include +#include +#include #include -#include +#include #include #include -#include -#include +#include +#include +#include +#include #include -#include +#include +#include -#include "../webserver.h" -#include "../websocketclient.h" #include "../draft/draftmanager.h" #include "../editor/composerwindow.h" #include "../editor/recipientseditor.h" +#include "../webserver.h" +#include "../websocketclient.h" -#include -#include #include +#include +#include using namespace Qt::Literals::StringLiterals; class EmailControllerTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase() { DraftManager::self(true); QCoreApplication::setApplicationName(u"gpgol-server"_s); KLocalizedString::setApplicationDomain(QByteArrayLiteral("gpgol")); m_webServer = new WebServer; m_webServer->run(); auto webSocketServer = new QWebSocketServer(QStringLiteral("SSL Server"), QWebSocketServer::NonSecureMode); - if (webSocketServer->listen(QHostAddress::Any, 5657)) { - - } + if (webSocketServer->listen(QHostAddress::Any, 5657)) { } } void testInfoEmailAction() { QFile file(QStringLiteral(DATA_DIR) + u"/encrypted.mbox"_s); QVERIFY(file.open(QIODeviceBase::ReadOnly)); QNetworkRequest request(QUrl(u"http://127.0.0.1:%1/info"_s.arg(m_webServer->port()))); auto reply = m_qnam.post(request, file.readAll()); QSignalSpy spy(reply, &QNetworkReply::finished); spy.wait(); QVERIFY(reply->error() == QNetworkReply::NoError); const auto doc = QJsonDocument::fromJson(reply->readAll()); QVERIFY(!doc.isNull() && doc.isObject()); const auto object = doc.object(); QVERIFY(object["drafts"_L1].toArray().isEmpty()); QVERIFY(object["encrypted"_L1].toBool()); QVERIFY(!object["signed"_L1].toBool()); } void testViewEmailAction() { QFile file(QStringLiteral(DATA_DIR) + u"/plaintext.mbox"_s); QVERIFY(file.open(QIODeviceBase::ReadOnly)); QNetworkRequest request(QUrl(u"http://127.0.0.1:%1/view"_s.arg(m_webServer->port()))); auto reply = m_qnam.post(request, file.readAll()); QSignalSpy spy(reply, &QNetworkReply::finished); spy.wait(); QVERIFY(reply->error() == QNetworkReply::NoError); const auto widgets = qApp->topLevelWidgets(); QVERIFY(!widgets.isEmpty()); MimeTreeParser::Widgets::MessageViewerDialog *dialog = nullptr; for (auto widget : widgets) { if (!widget->isHidden()) { if (const auto messageViewer = qobject_cast(widget)) { dialog = messageViewer; break; } } } QVERIFY(dialog); WebsocketClient::self(QUrl(u"ws://127.0.0.1"_s), 5656); const auto toolBar = dialog->toolBar(); QVERIFY(toolBar->isVisible()); const auto actions = toolBar->actions(); QCOMPARE(actions.count(), 3); qWarning() << actions; QCOMPARE(actions[1]->icon().name(), u"mail-reply-sender-symbolic"_s); actions[1]->trigger(); const auto widgets2 = qApp->topLevelWidgets(); QVERIFY(!widgets2.isEmpty()); ComposerWindow *composer = nullptr; for (auto widget : widgets2) { if (!widget->isHidden()) { if (const auto composerWindow = qobject_cast(widget)) { composer = composerWindow; break; } } } QVERIFY(composer); QSignalSpy spyInit(composer, &ComposerWindow::initialized); spyInit.wait(); QCOMPARE(composer->subject(), u"RE: A random subject with alternative contenttype"_s); const auto recipients = composer->recipientsEditor()->recipients(); QCOMPARE(recipients.count(), 2); QCOMPARE(recipients[0]->email(), u"konqi@example.org"_s); QCOMPARE(recipients[0]->name(), u"Konqi"_s); QCOMPARE(recipients[1]->email(), u"konqi@kde.org"_s); QVERIFY(recipients[1]->name().isEmpty()); } void cleanupTestCase() { m_webServer->deleteLater(); } private: WebServer *m_webServer = nullptr; QNetworkAccessManager m_qnam; }; QTEST_MAIN(EmailControllerTest) #include "emailcontrollertest.moc" diff --git a/client/autotests/itipjobtest.cpp b/client/autotests/itipjobtest.cpp index 73ba4e4..9ec3ecc 100644 --- a/client/autotests/itipjobtest.cpp +++ b/client/autotests/itipjobtest.cpp @@ -1,143 +1,143 @@ /* SPDX-FileCopyrightText: 2023 Daniel Vrátil SPDX-License-Identifier: LGPL-2.0-or-later */ #include "itipjobtest.h" #include "qtest_messagecomposer.h" #include #include #include #include using namespace KMime; #include "../editor/composer.h" -#include "../editor/part/globalpart.h" #include "../editor/job/itipjob.h" +#include "../editor/part/globalpart.h" #include "../editor/part/itippart.h" using namespace MessageComposer; QTEST_MAIN(ItipJobTest) static constexpr QLatin1StringView testItip(R"( BEGIN:VCALENDAR CALSCALE:GREGORIAN METHOD:REQUEST BEGIN:VEVENT CREATED:20230508T143456Z ORGANIZER;CN=Konqi:MAILTO:konqi@example.com ATTENDEE;CN=Kate;RSVP=TRUE;ROLE=REQ-PARTICIPANT:MAILTO:kate@example.com CREATED:20230508T143456Z UID:KOrganizer-1673850046.1067 SUMMARY:Krypto Party DTSTART;VALUE=DATE:20230520 DTEND;VALUE=DATE:20230520 END:VEVENT END:VCALENDAR)"); static constexpr QLatin1StringView testItipMessage("Hi all, let's do some crypto partying!"); void ItipJobTest::testInvitationWithAttachment() { auto part = std::make_unique(); part->setOutlookConformInvitation(false); part->setInvitation(testItip); part->setInvitationBody(testItipMessage); Composer composer; ItipJob job(part.get(), &composer); job.setAutoDelete(false); QVERIFY(job.exec()); auto *content = job.content(); content->assemble(); QVERIFY(content); QCOMPARE(content->contentType(false)->mimeType(), "multipart/mixed"); const auto subparts = content->contents(); QCOMPARE(subparts.size(), 2); const auto msgPart = subparts[0]; QCOMPARE(msgPart->contentType(false)->mimeType(), "text/plain"); QCOMPARE(msgPart->contentDisposition(false)->disposition(), KMime::Headers::CDinline); QCOMPARE(msgPart->decodedText(), testItipMessage); const auto itipPart = subparts[1]; QCOMPARE(itipPart->contentType(false)->mimeType(), "text/calendar"); QCOMPARE(itipPart->contentType(false)->name(), QStringLiteral("cal.ics")); QCOMPARE(itipPart->contentType(false)->parameter(QStringLiteral("method")), QStringLiteral("request")); QCOMPARE(itipPart->contentType(false)->charset(), "utf-8"); QCOMPARE(itipPart->contentDisposition(false)->disposition(), KMime::Headers::CDattachment); QCOMPARE(itipPart->decodedText(), testItip); } void ItipJobTest::testInvitationWithoutAttachment() { auto part = std::make_unique(); part->setOutlookConformInvitation(false); part->setInvitationBody(testItipMessage); Composer composer; ItipJob job(part.get(), &composer); job.setAutoDelete(false); QVERIFY(job.exec()); auto *content = job.content(); content->assemble(); QVERIFY(content); QCOMPARE(content->contentType(false)->mimeType(), "text/plain"); QCOMPARE(content->contentDisposition(false)->disposition(), KMime::Headers::CDinline); QCOMPARE(content->decodedText(), testItipMessage); } void ItipJobTest::testOutlookInvitationWithAttachment() { auto part = std::make_unique(); part->setOutlookConformInvitation(true); part->setInvitation(testItip); part->setInvitationBody(testItipMessage); Composer composer; ItipJob job(part.get(), &composer); job.setAutoDelete(false); QVERIFY(job.exec()); auto *content = job.content(); content->assemble(); QVERIFY(content); QCOMPARE(content->contentType(false)->mimeType(), "text/calendar"); QCOMPARE(content->contentType(false)->name(), QStringLiteral("cal.ics")); QCOMPARE(content->contentType(false)->parameter(QStringLiteral("method")), QStringLiteral("request")); QCOMPARE(content->contentType(false)->charset(), "utf-8"); QCOMPARE(content->contentDisposition(false)->disposition(), KMime::Headers::CDinline); QCOMPARE(content->decodedText(), testItip); } void ItipJobTest::testOutlookInvitationWithoutAttachment() { auto part = std::make_unique(); part->setOutlookConformInvitation(true); part->setInvitationBody(testItipMessage); Composer composer; ItipJob job(part.get(), &composer); job.setAutoDelete(false); QVERIFY(job.exec()); auto *content = job.content(); content->assemble(); QVERIFY(content); QCOMPARE(content->contentType(false)->mimeType(), "text/calendar"); QCOMPARE(content->contentType(false)->name(), QStringLiteral("cal.ics")); QCOMPARE(content->contentType(false)->parameter(QStringLiteral("method")), QStringLiteral("request")); QCOMPARE(content->contentType(false)->charset(), "utf-8"); QVERIFY(content->decodedText().isEmpty()); } #include "moc_itipjobtest.cpp" diff --git a/client/autotests/multipartjobtest.cpp b/client/autotests/multipartjobtest.cpp index 30f8d90..36ae383 100644 --- a/client/autotests/multipartjobtest.cpp +++ b/client/autotests/multipartjobtest.cpp @@ -1,96 +1,96 @@ /* SPDX-FileCopyrightText: 2009 Constantin Berzan SPDX-License-Identifier: LGPL-2.0-or-later */ #include "multipartjobtest.h" #include #include #include using namespace KMime; #include "../editor/composer.h" -#include "../editor/part/globalpart.h" #include "../editor/job/multipartjob.h" #include "../editor/job/singlepartjob.h" +#include "../editor/part/globalpart.h" using namespace MessageComposer; QTEST_MAIN(MultipartJobTest) void MultipartJobTest::testMultipartMixed() { Composer composer; auto mjob = new MultipartJob(&composer); mjob->setMultipartSubtype("mixed"); QByteArray data1("one"); QByteArray data2("two"); QByteArray type1("text/plain"); QByteArray type2("application/x-mors-ontologica"); { auto cjob = new SinglepartJob(mjob); cjob->setData(data1); cjob->contentType()->setMimeType(type1); } { auto cjob = new SinglepartJob(mjob); cjob->setData(data2); cjob->contentType()->setMimeType(type2); } QVERIFY(mjob->exec()); Content *result = mjob->content(); result->assemble(); qDebug() << result->encodedContent(); QVERIFY(result->contentType(false)); QCOMPARE(result->contentType(false)->mimeType(), QByteArray("multipart/mixed")); QCOMPARE(result->contents().count(), 2); { Content *c = result->contents().at(0); QCOMPARE(c->body(), data1); QVERIFY(c->contentType(false)); QCOMPARE(c->contentType(false)->mimeType(), type1); } { Content *c = result->contents().at(1); QCOMPARE(c->body(), data2); QVERIFY(c->contentType(false)); QCOMPARE(c->contentType(false)->mimeType(), type2); } delete result; } void MultipartJobTest::test8BitPropagation() { // If a subpart is 8bit, its parent must be 8bit too. Composer composer; composer.globalPart()->set8BitAllowed(true); auto mjob = new MultipartJob(&composer); mjob->setMultipartSubtype("mixed"); auto mjob2 = new MultipartJob(mjob); mjob2->setMultipartSubtype("mixed"); auto cjob = new SinglepartJob(mjob2); QByteArray data("time is so short and I'm sure there must be something more"); cjob->setData(data); cjob->contentTransferEncoding()->setEncoding(Headers::CE8Bit); QVERIFY(mjob->exec()); Content *content = mjob->content(); content->assemble(); qDebug() << content->encodedContent(); QVERIFY(content->contentTransferEncoding(false)); QCOMPARE(content->contentTransferEncoding(false)->encoding(), Headers::CE8Bit); delete content; } #include "moc_multipartjobtest.cpp" diff --git a/client/controllers/emailcontroller.cpp b/client/controllers/emailcontroller.cpp index 7188bec..d711a58 100644 --- a/client/controllers/emailcontroller.cpp +++ b/client/controllers/emailcontroller.cpp @@ -1,178 +1,164 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "emailcontroller.h" -#include -#include +#include #include -#include +#include +#include +#include #include +#include #include -#include #include -#include +#include "draft/draftmanager.h" #include "editor/composerwindow.h" #include "editor/composerwindowfactory.h" -#include "draft/draftmanager.h" #include using namespace Qt::Literals::StringLiterals; QHttpServerResponse EmailController::viewEmailAction(const QHttpServerRequest &request) { const auto email = QString::fromUtf8(Utils::findHeader(request.headers(), "X-EMAIL")); const auto displayName = QString::fromUtf8(Utils::findHeader(request.headers(), "X-NAME")); const auto bearerToken = Utils::findHeader(request.headers(), "X-TOKEN"); const auto content = request.body(); KMime::Message::Ptr message(new KMime::Message()); message->setContent(KMime::CRLFtoLF(content)); message->parse(); - auto dialog = new MimeTreeParser::Widgets::MessageViewerDialog({ message }); + auto dialog = new MimeTreeParser::Widgets::MessageViewerDialog({message}); dialog->setAttribute(Qt::WA_DeleteOnClose); auto toolBar = dialog->toolBar(); toolBar->show(); // spacer - QWidget* spacer = new QWidget(); + QWidget *spacer = new QWidget(); spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); toolBar->addWidget(spacer); // reply auto replyAction = new QAction(QIcon::fromTheme(u"mail-reply-sender-symbolic"_s), i18nc("@action:button", "Reply"), toolBar); QObject::connect(replyAction, &QAction::triggered, dialog, [message, email, displayName, bearerToken](bool) { auto dialog = ComposerWindowFactory::self().create(email, displayName, bearerToken); dialog->reply(message); dialog->show(); }); toolBar->addAction(replyAction); auto widget = qobject_cast(toolBar->widgetForAction(replyAction)); widget->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); // forward auto forwardAction = new QAction(QIcon::fromTheme(u"mail-forward-symbolic"_s), i18nc("@action:button", "Forward"), toolBar); QObject::connect(forwardAction, &QAction::triggered, dialog, [message, email, displayName, bearerToken](bool) { auto dialog = ComposerWindowFactory::self().create(email, displayName, bearerToken); dialog->reply(message); dialog->show(); }); toolBar->addAction(forwardAction); widget = qobject_cast(toolBar->widgetForAction(forwardAction)); widget->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); dialog->show(); - return QHttpServerResponse(QJsonObject { - { "status"_L1, "200"_L1 } - }); + return QHttpServerResponse(QJsonObject{{"status"_L1, "200"_L1}}); } QHttpServerResponse EmailController::infoEmailAction(const QHttpServerRequest &request) { qDebug() << "request received"; const auto content = request.body(); KMime::Message::Ptr message(new KMime::Message()); message->setContent(KMime::CRLFtoLF(content)); message->parse(); MimeTreeParser::ObjectTreeParser treeParser; treeParser.parseObjectTree(message.get()); - return QHttpServerResponse(QJsonObject { - { "status"_L1, "200"_L1 }, - { "encrypted"_L1, treeParser.hasEncryptedParts() }, - { "signed"_L1, treeParser.hasSignedParts() }, - { "drafts"_L1, DraftManager::self().toJson() }, + return QHttpServerResponse(QJsonObject{ + {"status"_L1, "200"_L1}, + {"encrypted"_L1, treeParser.hasEncryptedParts()}, + {"signed"_L1, treeParser.hasSignedParts()}, + {"drafts"_L1, DraftManager::self().toJson()}, }); } QHttpServerResponse EmailController::newEmailAction(const QHttpServerRequest &request) { const auto email = QString::fromUtf8(Utils::findHeader(request.headers(), "X-EMAIL")); const auto displayName = QString::fromUtf8(Utils::findHeader(request.headers(), "X-NAME")); const auto bearerToken = Utils::findHeader(request.headers(), "X-TOKEN"); auto dialog = ComposerWindowFactory::self().create(email, displayName, bearerToken); dialog->show(); - return QHttpServerResponse(QJsonObject { - { "status"_L1, "200"_L1 } - }); + return QHttpServerResponse(QJsonObject{{"status"_L1, "200"_L1}}); } QHttpServerResponse EmailController::replyEmailAction(const QHttpServerRequest &request) { const auto email = QString::fromUtf8(Utils::findHeader(request.headers(), "X-EMAIL")); const auto displayName = QString::fromUtf8(Utils::findHeader(request.headers(), "X-NAME")); const auto bearerToken = Utils::findHeader(request.headers(), "X-TOKEN"); const auto content = request.body(); KMime::Message::Ptr message(new KMime::Message()); message->setContent(KMime::CRLFtoLF(content)); message->parse(); auto dialog = ComposerWindowFactory::self().create(email, displayName, bearerToken); dialog->reply(message); dialog->show(); - return QHttpServerResponse(QJsonObject { - { "status"_L1, "200"_L1 } - }); + return QHttpServerResponse(QJsonObject{{"status"_L1, "200"_L1}}); } QHttpServerResponse EmailController::forwardEmailAction(const QHttpServerRequest &request) { const auto email = QString::fromUtf8(Utils::findHeader(request.headers(), "X-EMAIL")); const auto displayName = QString::fromUtf8(Utils::findHeader(request.headers(), "X-NAME")); const auto bearerToken = Utils::findHeader(request.headers(), "X-TOKEN"); const auto content = request.body(); KMime::Message::Ptr message(new KMime::Message()); message->setContent(KMime::CRLFtoLF(content)); message->parse(); auto dialog = ComposerWindowFactory::self().create(email, displayName, bearerToken); dialog->forward(message); dialog->show(); - return QHttpServerResponse(QJsonObject { - { "status"_L1, "200"_L1 } - }); + return QHttpServerResponse(QJsonObject{{"status"_L1, "200"_L1}}); } QHttpServerResponse EmailController::draftAction(QString draftId, const QHttpServerRequest &request) { const auto email = QString::fromUtf8(Utils::findHeader(request.headers(), "X-EMAIL")); const auto displayName = QString::fromUtf8(Utils::findHeader(request.headers(), "X-NAME")); const auto bearerToken = Utils::findHeader(request.headers(), "X-TOKEN"); const auto draft = DraftManager::self().draftById(draftId.toUtf8()); if (!draft.isValid()) { - return QHttpServerResponse(QJsonObject { - { "status"_L1, "404"_L1 } - }, QHttpServerResponse::StatusCode::NotFound); + return QHttpServerResponse(QJsonObject{{"status"_L1, "404"_L1}}, QHttpServerResponse::StatusCode::NotFound); } if (request.method() == QHttpServerRequest::Method::Post) { // POST auto dialog = ComposerWindowFactory::self().create(email, displayName, bearerToken); dialog->setMessage(draft.mime()); dialog->show(); } else { // DELETE if (!DraftManager::self().remove(draft)) { - return QHttpServerResponse(QJsonObject { - { "status"_L1, "500"_L1 } - }, QHttpServerResponse::StatusCode::InternalServerError); + return QHttpServerResponse(QJsonObject{{"status"_L1, "500"_L1}}, QHttpServerResponse::StatusCode::InternalServerError); } } - return QHttpServerResponse(QJsonObject { - { "status"_L1, "200"_L1 } - }); + return QHttpServerResponse(QJsonObject{{"status"_L1, "200"_L1}}); } diff --git a/client/draft/draft.cpp b/client/draft/draft.cpp index 76613b0..8d56422 100644 --- a/client/draft/draft.cpp +++ b/client/draft/draft.cpp @@ -1,69 +1,68 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "draft.h" #include #include "editor_debug.h" using namespace Qt::Literals::StringLiterals; Draft::Draft(const QString &localUrl) : m_localUrl(localUrl) , m_fileInfo(m_localUrl) { } bool Draft::isValid() const { return m_fileInfo.exists() && m_fileInfo.isReadable(); } QJsonObject Draft::toJson() const { return { - { "id"_L1, m_fileInfo.fileName() }, - { "url"_L1, m_localUrl }, - { "last_modification"_L1, lastModified().toSecsSinceEpoch() }, + {"id"_L1, m_fileInfo.fileName()}, + {"url"_L1, m_localUrl}, + {"last_modification"_L1, lastModified().toSecsSinceEpoch()}, }; } QString Draft::localUrl() const { return m_localUrl; } QDateTime Draft::lastModified() const { return m_fileInfo.lastModified(); } bool Draft::remove() { QFile file(m_fileInfo.filePath()); if (!file.exists()) { qCWarning(EDITOR_LOG) << "File doesn't exist anymore."; return false; } return file.remove(); } - KMime::Message::Ptr Draft::mime() const { Q_ASSERT(isValid()); // should be checked by the caller QFile file(m_fileInfo.filePath()); if (!file.open(QIODeviceBase::ReadOnly)) { qFatal() << "Can open file" << m_fileInfo.filePath(); } KMime::Message::Ptr message(new KMime::Message()); message->setContent(KMime::CRLFtoLF(file.readAll())); message->assemble(); return message; } diff --git a/client/draft/draft.h b/client/draft/draft.h index be428d4..fd7592c 100644 --- a/client/draft/draft.h +++ b/client/draft/draft.h @@ -1,50 +1,49 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include -#include #include +#include +#include #include class DraftManager; /// @class Draft /// Represent an email draft in the filesytem. class Draft { public: /// Load the draft from it's local uri. explicit Draft(const QString &localUrl); /// Returns whether this Draft object is valid. bool isValid() const; /// Convert the draft meta information to a JSON object. QJsonObject toJson() const; /// Local url of the draft in the filesystem. QString localUrl() const; QDateTime lastModified() const; KMime::Message::Ptr mime() const; private: friend DraftManager; /// Remove the draft from the filesystem. bool remove(); QString m_localUrl; QFileInfo m_fileInfo; }; -inline bool operator==(const Draft& lhs, const Draft& rhs) +inline bool operator==(const Draft &lhs, const Draft &rhs) { return lhs.localUrl() == rhs.localUrl(); } - diff --git a/client/draft/draftmanager.cpp b/client/draft/draftmanager.cpp index 3cd8074..383d923 100644 --- a/client/draft/draftmanager.cpp +++ b/client/draft/draftmanager.cpp @@ -1,89 +1,89 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "draftmanager.h" #include #include #include "editor_debug.h" DraftManager::DraftManager(bool testMode) : m_testMode(testMode) { const QDir directory(draftDirectory(testMode)); const auto entries = directory.entryList(QDir::Files); for (const QString &entry : entries) { Draft draft(draftDirectory() + entry); if (draft.isValid()) { m_drafts << draft; } else { - qFatal(EDITOR_LOG) << "File does not exist or is not readable" << entry ; + qFatal(EDITOR_LOG) << "File does not exist or is not readable" << entry; } } } QString DraftManager::draftDirectory(bool testMode) { if (testMode) { static const QString path = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + QLatin1String("/gpgol-server/draft/"); return path; } else { static const QString path = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/gpgol-server/draft/"); return path; } } QString DraftManager::autosaveDirectory(bool testMode) { if (testMode) { static const QString path = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + QLatin1String("/gpgol-server/autosave/"); return path; } else { static const QString path = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/gpgol-server/autosave/"); return path; } } DraftManager &DraftManager::self(bool testMode) { static DraftManager s_draftManager(testMode); return s_draftManager; } QList DraftManager::drafts() const { return m_drafts; } QJsonArray DraftManager::toJson() const { if (m_drafts.isEmpty()) { return {}; } QJsonArray array; std::transform(m_drafts.cbegin(), m_drafts.cend(), std::back_inserter(array), [](const auto draft) { return draft.toJson(); }); return array; } bool DraftManager::remove(const Draft &draft) { auto it = std::find(m_drafts.begin(), m_drafts.end(), draft); if (it == m_drafts.end()) { return false; } bool ok = it->remove(); m_drafts.erase(it); return ok; } Draft DraftManager::draftById(const QByteArray &draftId) { return Draft(draftDirectory() + QString::fromUtf8(draftId)); } diff --git a/client/draft/draftmanager.h b/client/draft/draftmanager.h index cdeb25f..3d1dfe0 100644 --- a/client/draft/draftmanager.h +++ b/client/draft/draftmanager.h @@ -1,43 +1,44 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "draft.h" #include /// @class DraftManager /// /// Manage the email drafts of the user. class DraftManager { public: /// Get the DraftManager singleton. /// \param testMode Set it to true when running unit tests. static DraftManager &self(bool testMode = false); /// Get the directory where the drafts are stored. /// \param testMode Set it to true when running unit tests. static QString draftDirectory(bool testMode = false); /// Get the directory where the autosaved drafts are stored. /// \param testMode Set it to true when running unit tests. static QString autosaveDirectory(bool testMode = false); /// List of drafts. [[nodiscard]] QList drafts() const; /// List of drafts as JSON array. [[nodiscard]] QJsonArray toJson() const; /// Remove the specified dradt from the filesystem. bool remove(const Draft &draft); /// Get a draft by it's id. Draft draftById(const QByteArray &draftId); + private: DraftManager(bool testMode); QList m_drafts; bool m_testMode = false; }; \ No newline at end of file diff --git a/client/editor/addresseelineedit.cpp b/client/editor/addresseelineedit.cpp index 700c220..8717918 100644 --- a/client/editor/addresseelineedit.cpp +++ b/client/editor/addresseelineedit.cpp @@ -1,1169 +1,1169 @@ /* This file is part of libkdepim. SPDX-FileCopyrightText: 2002 Helge Deller SPDX-FileCopyrightText: 2002 Lubos Lunak SPDX-FileCopyrightText: 2001, 2003 Carsten Pfeiffer SPDX-FileCopyrightText: 2001 Waldo Bastian SPDX-FileCopyrightText: 2004 Daniel Molkentin SPDX-FileCopyrightText: 2004 Karl-Heinz Zimmer SPDX-FileCopyrightText: 2017-2024 Laurent Montel SPDX-License-Identifier: LGPL-2.0-or-later */ #include "addresseelineedit.h" #include "addresseelineeditmanager.h" #include "kmailcompletion.h" -#include +#include +#include #include -#include +#include #include +#include #include +#include #include #include -#include -#include -#include -#include +#include +#include +#include #include #include -#include +#include #include -#include -#include -#include #include +#include using namespace std::chrono_literals; using namespace Qt::Literals::StringLiterals; static constexpr QLatin1StringView s_completionItemIndentString(" "); inline bool itemIsHeader(const QListWidgetItem *item) { return item && !item->text().startsWith(s_completionItemIndentString); } QString adaptPasteMails(const QString &str) { QString newText = str; // remove newlines in the to-be-pasted string static QRegularExpression reg2(QStringLiteral("\r?\n")); QStringList lines = newText.split(reg2, Qt::SkipEmptyParts); QStringList::iterator end(lines.end()); for (QStringList::iterator it = lines.begin(); it != end; ++it) { // remove trailing commas and whitespace static QRegularExpression reg1(QRegularExpression(QStringLiteral(",?\\s*$"))); it->remove(reg1); } newText = lines.join(QLatin1StringView(", ")); if (newText.startsWith(QLatin1StringView("mailto:"))) { const QUrl url(newText); newText = url.path(); } else if (newText.contains(QLatin1StringView(" at "))) { // Anti-spam stuff newText.replace(QStringLiteral(" at "), QStringLiteral("@")); newText.replace(QStringLiteral(" dot "), QStringLiteral(".")); } else if (newText.contains(QLatin1StringView("(at)"))) { static QRegularExpression reg((QStringLiteral("\\s*\\(at\\)\\s*"))); newText.replace(reg, QStringLiteral("@")); } return newText; } class SourceWithWeight { public: int weight; // the weight of the source int index; // index into AddresseeLineEditStatic::self()->completionSources QString sourceName; // the name of the source, e.g. "LDAP Server" bool operator<(const SourceWithWeight &other) const { if (weight > other.weight) { return true; } if (weight < other.weight) { return false; } return sourceName < other.sourceName; } }; class AddresseeLineEditPrivate : public QObject { Q_OBJECT public: AddresseeLineEditPrivate(AddresseeLineEdit *qq); void setCompletedItems(const QStringList &items, bool autoSuggest); void addCompletionItem(const QString &string, int weight, int completionItemSource, const QStringList *keyWords = nullptr); bool smartPaste() const; void setSmartPaste(bool smartPaste); bool completionInitialized() const; bool useCompletion() const; void setUseCompletion(bool useCompletion); const QStringList adjustedCompletionItems(bool fullSearch); void updateSearchString(); void startSearches(); void slotCompletion(); void doCompletion(bool ctrlT); void searchInGnupg(); void slotTriggerDelayedQueries(); [[nodiscard]] QString searchString() const; void setSearchString(const QString &searchString); [[nodiscard]] bool searchExtended() const; void setSearchExtended(bool searchExtended); [[nodiscard]] bool useSemicolonAsSeparator() const; void setUseSemicolonAsSeparator(bool useSemicolonAsSeparator); void slotReturnPressed(const QString &); void slotPopupCompletion(const QString &completion); void slotUserCancelled(const QString &cancelText); private: - AddresseeLineEdit * const q; + AddresseeLineEdit *const q; QString mSearchString; QString mPreviousAddresses; bool mSmartPaste; bool mUseCompletion = true; bool mCompletionInitialized = false; bool mLastSearchMode; bool mSearchExtended = false; bool mUseSemicolonAsSeparator = false; QTimer *mDelayedQueryTimer; }; AddresseeLineEditPrivate::AddresseeLineEditPrivate(AddresseeLineEdit *qq) : QObject(qq) , q(qq) , mDelayedQueryTimer(new QTimer(this)) { mDelayedQueryTimer->setSingleShot(true); connect(mDelayedQueryTimer, &QTimer::timeout, this, &AddresseeLineEditPrivate::slotTriggerDelayedQueries); if (!mCompletionInitialized) { q->setCompletionObject(AddresseeLineEditManager::self()->completion(), false); connect(q, &KLineEdit::completion, this, &AddresseeLineEditPrivate::slotCompletion); connect(q, &AddresseeLineEdit::returnKeyPressed, this, &AddresseeLineEditPrivate::slotReturnPressed); KCompletionBox *box = q->completionBox(); connect(box, &KCompletionBox::textActivated, this, &AddresseeLineEditPrivate::slotPopupCompletion); connect(box, &KCompletionBox::userCancelled, this, &AddresseeLineEditPrivate::slotUserCancelled); mCompletionInitialized = true; } connect(q, &AddresseeLineEdit::textCompleted, q, &AddresseeLineEdit::slotEditingFinished); connect(q, &AddresseeLineEdit::editingFinished, q, &AddresseeLineEdit::slotEditingFinished); AddresseeLineEditManager::self()->addCompletionSource(i18n("GPG keychain"), 100); } void AddresseeLineEditPrivate::slotUserCancelled(const QString &cancelText) { q->callUserCancelled(mPreviousAddresses + cancelText); // in KLineEdit } void AddresseeLineEditPrivate::slotPopupCompletion(const QString &completion) { QString c = completion.trimmed(); if (c.endsWith(QLatin1Char(')'))) { c = completion.mid(0, completion.lastIndexOf(QLatin1StringView(" ("))).trimmed(); } q->setText(mPreviousAddresses + c); q->cursorAtEnd(); updateSearchString(); q->emitTextCompleted(); } - void AddresseeLineEditPrivate::slotReturnPressed(const QString &) { if (!q->completionBox()->selectedItems().isEmpty()) { slotPopupCompletion(q->completionBox()->selectedItems().constFirst()->text()); } } bool AddresseeLineEditPrivate::useSemicolonAsSeparator() const { return mUseSemicolonAsSeparator; } void AddresseeLineEditPrivate::setUseSemicolonAsSeparator(bool useSemicolonAsSeparator) { mUseSemicolonAsSeparator = useSemicolonAsSeparator; } const QStringList AddresseeLineEditPrivate::adjustedCompletionItems(bool fullSearch) { QStringList items = fullSearch ? AddresseeLineEditManager::self()->completion()->allMatches(mSearchString) : AddresseeLineEditManager::self()->completion()->substringCompletion(mSearchString); // force items to be sorted by email items.sort(); // For weighted mode, the algorithm is the following: // In the first loop, we add each item to its section (there is one section per completion source) // We also add spaces in front of the items. // The sections are appended to the items list. // In the second loop, we then walk through the sections and add all the items in there to the // sorted item list, which is the final result. // // The algo for non-weighted mode is different. int lastSourceIndex = -1; // Maps indices of the items list, which are section headers/source items, // to a QStringList which are the items of that section/source. QMap sections; QStringList sortedItems; for (QStringList::Iterator it = items.begin(); it != items.end(); ++it) { auto cit = AddresseeLineEditManager::self()->completionItemsMap.constFind(*it); if (cit == AddresseeLineEditManager::self()->completionItemsMap.constEnd()) { continue; } const int index = (*cit).second; if (AddresseeLineEditManager::self()->completion()->order() == KCompletion::Weighted) { if (lastSourceIndex == -1 || lastSourceIndex != index) { const QString sourceLabel(AddresseeLineEditManager::self()->completionSources.at(index)); if (sections.find(index) == sections.end()) { it = items.insert(it, sourceLabel); ++it; // skip new item } lastSourceIndex = index; } it->prepend(s_completionItemIndentString); // remove preferred email sort added in addContact() it->replace(QLatin1StringView(" <"), QStringLiteral(" <")); } sections[index].append(*it); if (AddresseeLineEditManager::self()->completion()->order() == KCompletion::Sorted) { sortedItems.append(*it); } } if (AddresseeLineEditManager::self()->completion()->order() == KCompletion::Weighted) { // Sort the sections QList sourcesAndWeights; const int numberOfCompletionSources(AddresseeLineEditManager::self()->completionSources.count()); sourcesAndWeights.reserve(numberOfCompletionSources); for (int i = 0; i < numberOfCompletionSources; ++i) { SourceWithWeight sww; sww.sourceName = AddresseeLineEditManager::self()->completionSources.at(i); sww.weight = AddresseeLineEditManager::self()->completionSourceWeights[sww.sourceName]; sww.index = i; sourcesAndWeights.append(sww); } std::sort(sourcesAndWeights.begin(), sourcesAndWeights.end()); // Add the sections and their items to the final sortedItems result list const int numberOfSources(sourcesAndWeights.size()); for (int i = 0; i < numberOfSources; ++i) { const SourceWithWeight source = sourcesAndWeights.at(i); const QStringList sectionItems = sections[source.index]; if (!sectionItems.isEmpty()) { sortedItems.append(source.sourceName); for (const QString &itemInSection : sectionItems) { sortedItems.append(itemInSection); } } } } else { sortedItems.sort(); } return sortedItems; } void AddresseeLineEditPrivate::doCompletion(bool ctrlT) { mLastSearchMode = ctrlT; const KCompletion::CompletionMode mode = q->completionMode(); if (mode == KCompletion::CompletionNone) { return; } AddresseeLineEditManager::self()->completion()->setOrder(KCompletion::Weighted); // cursor at end of string - or Ctrl+T pressed for substring completion? if (ctrlT) { const QStringList completions = adjustedCompletionItems(false); if (completions.count() == 1) { q->setText(mPreviousAddresses + completions.first().trimmed()); } // Make sure the completion popup is closed if no matching items were found setCompletedItems(completions, true); q->cursorAtEnd(); q->setCompletionMode(mode); // set back to previous mode return; } switch (mode) { case KCompletion::CompletionPopupAuto: if (mSearchString.isEmpty()) { break; } // else: fall-through to the CompletionPopup case [[fallthrough]]; case KCompletion::CompletionPopup: { const QStringList items = adjustedCompletionItems(false); setCompletedItems(items, false); break; } case KCompletion::CompletionShell: { const QString match = AddresseeLineEditManager::self()->completion()->makeCompletion(mSearchString); if (!match.isNull() && match != mSearchString) { q->setText(mPreviousAddresses + match); q->setModified(true); q->cursorAtEnd(); } break; } case KCompletion::CompletionMan: // Short-Auto in fact case KCompletion::CompletionAuto: // force autoSuggest in KLineEdit::keyPressed or setCompletedText will have no effect q->setCompletionMode(q->completionMode()); if (!mSearchString.isEmpty()) { // if only our \" is left, remove it since user has not typed it either if (mSearchExtended && mSearchString == QLatin1StringView("\"")) { mSearchExtended = false; mSearchString.clear(); q->setText(mPreviousAddresses); break; } QString match = AddresseeLineEditManager::self()->completion()->makeCompletion(mSearchString); if (!match.isEmpty()) { if (match != mSearchString) { const QString adds = mPreviousAddresses + match; q->callSetCompletedText(adds); } } else { if (!mSearchString.startsWith(QLatin1Char('\"'))) { // try with quoted text, if user has not type one already match = AddresseeLineEditManager::self()->completion()->makeCompletion(QLatin1StringView("\"") + mSearchString); if (!match.isEmpty() && match != mSearchString) { mSearchString = QLatin1StringView("\"") + mSearchString; mSearchExtended = true; q->setText(mPreviousAddresses + mSearchString); q->callSetCompletedText(mPreviousAddresses + match); } } else if (mSearchExtended) { // our added \" does not work anymore, remove it mSearchString.remove(0, 1); mSearchExtended = false; q->setText(mPreviousAddresses + mSearchString); // now try again match = AddresseeLineEditManager::self()->completion()->makeCompletion(mSearchString); if (!match.isEmpty() && match != mSearchString) { const QString adds = mPreviousAddresses + match; q->setCompletedText(adds); } } } } break; case KCompletion::CompletionNone: break; } } void AddresseeLineEditPrivate::setCompletedItems(const QStringList &items, bool autoSuggest) { KCompletionBox *completionBox = q->completionBox(); if (!items.isEmpty() && !(items.count() == 1 && mSearchString == items.first())) { completionBox->clear(); const int numberOfItems(items.count()); for (int i = 0; i < numberOfItems; ++i) { auto item = new QListWidgetItem(items.at(i), completionBox); if (!items.at(i).startsWith(s_completionItemIndentString)) { item->setFlags(item->flags() & ~Qt::ItemIsSelectable); item->setBackground(AddresseeLineEditManager::self()->alternateColor()); } completionBox->addItem(item); } if (!completionBox->isVisible()) { if (!mSearchString.isEmpty()) { completionBox->setCancelledText(mSearchString); } completionBox->popup(); // we have to install the event filter after popup(), since that // calls show(), and that's where KCompletionBox installs its filter. // We want to be first, though, so do it now. if (AddresseeLineEditManager::self()->completion()->order() == KCompletion::Weighted) { qApp->installEventFilter(q); } } QListWidgetItem *item = completionBox->item(1); if (item) { completionBox->blockSignals(true); completionBox->setCurrentItem(item); item->setSelected(true); completionBox->blockSignals(false); } if (autoSuggest) { const int index = items.first().indexOf(mSearchString); const QString newText = items.first().mid(index); q->callSetUserSelection(false); q->callSetCompletedText(newText, true); } } else { if (completionBox && completionBox->isVisible()) { completionBox->hide(); completionBox->setItems(QStringList()); } } } void AddresseeLineEditPrivate::addCompletionItem(const QString &string, int weight, int completionItemSource, const QStringList *keyWords) { // Check if there is an exact match for item already, and use the // maximum weight if so. Since there's no way to get the information // from KCompletion, we have to keep our own QMap. // We also update the source since the item should always be shown from the source with the highest weight auto manager = AddresseeLineEditManager::self(); auto it = manager->completionItemsMap.find(string); if (it != manager->completionItemsMap.end()) { weight = qMax((*it).first, weight); (*it).first = weight; (*it).second = completionItemSource; } else { manager->completionItemsMap.insert(string, qMakePair(weight, completionItemSource)); } manager->completion()->addItem(string, weight); if (keyWords && !keyWords->isEmpty()) { manager->completion()->addItemWithKeys(string, weight, keyWords); // see kmailcompletion.cpp } } bool AddresseeLineEditPrivate::smartPaste() const { return mSmartPaste; } void AddresseeLineEditPrivate::setSmartPaste(bool smartPaste) { mSmartPaste = smartPaste; } bool AddresseeLineEditPrivate::completionInitialized() const { return mCompletionInitialized; } bool AddresseeLineEditPrivate::useCompletion() const { return mUseCompletion; } void AddresseeLineEditPrivate::setUseCompletion(bool useCompletion) { mUseCompletion = useCompletion; } AddresseeLineEdit::AddresseeLineEdit(QWidget *parent) : KLineEdit(parent) , d(std::make_unique(this)) -{} +{ +} AddresseeLineEdit::~AddresseeLineEdit() = default; void AddresseeLineEdit::addContactGroup(const KContacts::ContactGroup &group, int weight, int source) { d->addCompletionItem(group.name(), weight, source); } void AddresseeLineEdit::addContact(const QStringList &emails, const KContacts::Addressee &addr, int weight, int source, QString append) { int isPrefEmail = 1; // first in list is preferredEmail for (const QString &email : emails) { // TODO: highlight preferredEmail const QString givenName = addr.givenName(); const QString familyName = addr.familyName(); const QString nickName = addr.nickName(); const QString fullEmail = addr.fullEmail(email); QString appendix; if (!append.isEmpty()) { appendix = QStringLiteral(" (%1)"); append.replace(QLatin1Char('('), QStringLiteral("[")); append.replace(QLatin1Char(')'), QStringLiteral("]")); appendix = appendix.arg(append); } // Prepare "givenName" + ' ' + "familyName" QString fullName = givenName; if (!familyName.isEmpty()) { if (!fullName.isEmpty()) { fullName += QLatin1Char(' '); } fullName += familyName; } // Finally, we can add the completion items if (!fullName.isEmpty()) { const QString address = KEmailAddress::normalizedAddress(fullName, email, QString()); if (fullEmail != address) { // This happens when fullEmail contains a middle name, while our own fullName+email only has "first last". // Let's offer both, the fullEmail with 3 parts, looks a tad formal. d->addCompletionItem(address + appendix, weight + isPrefEmail, source); } } QStringList keyWords; if (!nickName.isEmpty()) { keyWords.append(nickName); } d->addCompletionItem(fullEmail + appendix, weight + isPrefEmail, source, &keyWords); isPrefEmail = 0; } } void AddresseeLineEdit::addContact(const KContacts::Addressee &addr, int weight, int source, const QString &append) { const QStringList emails = addr.emails(); if (emails.isEmpty()) { return; } addContact(emails, addr, weight, source, append); } void AddresseeLineEdit::dropEvent(QDropEvent *event) { const QMimeData *md = event->mimeData(); // Case one: The user dropped a text/directory (i.e. vcard), so decode its // contents if (KContacts::VCardDrag::canDecode(md)) { KContacts::Addressee::List list; KContacts::VCardDrag::fromMimeData(md, list); for (const KContacts::Addressee &addr : std::as_const(list)) { insertEmails(addr.emails()); } } // Case two: The user dropped a list or Urls. // Iterate over that list. For mailto: Urls, just add the addressee to the list, // and for other Urls, download the Url and assume it points to a vCard else if (md->hasUrls()) { const QList urls = md->urls(); KContacts::Addressee::List list; for (const QUrl &url : urls) { // First, let's deal with mailto Urls. The path() part contains the // email-address. if (url.scheme() == QLatin1StringView("mailto")) { KContacts::Addressee addressee; KContacts::Email email(KEmailAddress::decodeMailtoUrl(url)); email.setPreferred(true); addressee.addEmail(email); list += addressee; } } // Now, let the user choose which addressee to add. for (const KContacts::Addressee &addressee : std::as_const(list)) { insertEmails(addressee.emails()); } } // Case three: Let AddresseeLineEdit deal with the rest else { if (!isReadOnly()) { const QList uriList = event->mimeData()->urls(); if (!uriList.isEmpty()) { QString contents = text(); // remove trailing white space and comma int eot = contents.length(); while ((eot > 0) && contents.at(eot - 1).isSpace()) { --eot; } if (eot == 0) { contents.clear(); } else if (contents.at(eot - 1) == QLatin1Char(',')) { --eot; contents.truncate(eot); } bool mailtoURL = false; // append the mailto URLs for (const QUrl &url : uriList) { if (url.scheme() == QLatin1StringView("mailto")) { mailtoURL = true; QString address; address = QUrl::fromPercentEncoding(url.path().toLatin1()); address = KCodecs::decodeRFC2047String(address); if (!contents.isEmpty()) { contents.append(QLatin1StringView(", ")); } contents.append(address); } } if (mailtoURL) { setText(contents); setModified(true); return; } } else { // Let's see if this drop contains a comma separated list of emails if (md->hasText()) { const QString dropData = md->text(); const QStringList addrs = KEmailAddress::splitAddressList(dropData); if (!addrs.isEmpty()) { if (addrs.count() == 1) { QUrl url(dropData); if (url.scheme() == QLatin1StringView("mailto")) { KContacts::Addressee addressee; KContacts::Email email(KEmailAddress::decodeMailtoUrl(url)); email.setPreferred(true); addressee.addEmail(email); insertEmails(addressee.emails()); } else { setText(KEmailAddress::normalizeAddressesAndDecodeIdn(dropData)); } } else { setText(KEmailAddress::normalizeAddressesAndDecodeIdn(dropData)); } setModified(true); return; } } } } if (d->useCompletion()) { d->setSmartPaste(true); } QLineEdit::dropEvent(event); d->setSmartPaste(false); } } void AddresseeLineEdit::insertEmails(const QStringList &emails) { if (emails.empty()) { return; } QString contents = text(); if (!contents.isEmpty()) { contents += QLatin1Char(','); } // only one address, don't need kpopup to choose if (emails.size() == 1) { setText(contents + emails.front()); return; } // multiple emails, let the user choose one QMenu menu(this); menu.setTitle(i18n("Select email from contact")); menu.setObjectName(QLatin1StringView("Addresschooser")); for (const QString &email : emails) { menu.addAction(email); } const QAction *result = menu.exec(QCursor::pos()); if (!result) { return; } setText(contents + KLocalizedString::removeAcceleratorMarker(result->text())); } void AddresseeLineEdit::cursorAtEnd() { setCursorPosition(text().length()); } void AddresseeLineEdit::enableCompletion(bool enable) { d->setUseCompletion(enable); } bool AddresseeLineEdit::isCompletionEnabled() const { return d->useCompletion(); } bool AddresseeLineEdit::eventFilter(QObject *object, QEvent *event) { if (d->completionInitialized() && (object == completionBox() || completionBox()->findChild(object->objectName()) == object)) { if (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseMove || event->type() == QEvent::MouseButtonRelease || event->type() == QEvent::MouseButtonDblClick) { const QMouseEvent *mouseEvent = static_cast(event); // find list box item at the event position QListWidgetItem *item = completionBox()->itemAt(mouseEvent->pos()); if (!item) { // In the case of a mouse move outside of the box we don't want // the parent to fuzzy select a header by mistake. const bool eat = event->type() == QEvent::MouseMove; return eat; } // avoid selection of headers on button press, or move or release while // a button is pressed const Qt::MouseButtons buttons = mouseEvent->buttons(); if (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonDblClick || buttons & Qt::LeftButton || buttons & Qt::MiddleButton || buttons & Qt::RightButton) { if (itemIsHeader(item)) { return true; // eat the event, we don't want anything to happen } else { // if we are not on one of the group heading, make sure the item // below or above is selected, not the heading, inadvertedly, due // to fuzzy auto-selection from QListBox completionBox()->setCurrentItem(item); item->setSelected(true); if (event->type() == QEvent::MouseMove) { return true; // avoid fuzzy selection behavior } } } } } if ((object == this) && (event->type() == QEvent::ShortcutOverride)) { auto keyEvent = static_cast(event); if (keyEvent->key() == Qt::Key_Up || keyEvent->key() == Qt::Key_Down || keyEvent->key() == Qt::Key_Tab) { keyEvent->accept(); return true; } } if ((object == this) && (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) && completionBox()->isVisible()) { const QKeyEvent *keyEvent = static_cast(event); int currentIndex = completionBox()->currentRow(); if (currentIndex < 0) { return true; } if (keyEvent->key() == Qt::Key_Up) { // qCDebug(PIMCOMMONAKONADI_LOG) <<"EVENTFILTER: Qt::Key_Up currentIndex=" << currentIndex; // figure out if the item we would be moving to is one we want // to ignore. If so, go one further const QListWidgetItem *itemAbove = completionBox()->item(currentIndex); if (itemAbove && itemIsHeader(itemAbove)) { // there is a header above is, check if there is even further up // and if so go one up, so it'll be selected if (currentIndex > 0 && completionBox()->item(currentIndex - 1)) { // qCDebug(PIMCOMMONAKONADI_LOG) <<"EVENTFILTER: Qt::Key_Up -> skipping" << currentIndex - 1; completionBox()->setCurrentRow(currentIndex - 1); completionBox()->item(currentIndex - 1)->setSelected(true); } else if (currentIndex == 0) { // nothing to skip to, let's stay where we are, but make sure the // first header becomes visible, if we are the first real entry completionBox()->scrollToItem(completionBox()->item(0)); QListWidgetItem *item = completionBox()->item(currentIndex); if (item) { if (itemIsHeader(item)) { currentIndex++; item = completionBox()->item(currentIndex); } completionBox()->setCurrentItem(item); item->setSelected(true); } } return true; } } else if (keyEvent->key() == Qt::Key_Down) { // same strategy for downwards // qCDebug(PIMCOMMONAKONADI_LOG) <<"EVENTFILTER: Qt::Key_Down. currentIndex=" << currentIndex; const QListWidgetItem *itemBelow = completionBox()->item(currentIndex); if (itemBelow && itemIsHeader(itemBelow)) { if (completionBox()->item(currentIndex + 1)) { // qCDebug(PIMCOMMONAKONADI_LOG) <<"EVENTFILTER: Qt::Key_Down -> skipping" << currentIndex+1; completionBox()->setCurrentRow(currentIndex + 1); completionBox()->item(currentIndex + 1)->setSelected(true); } else { // nothing to skip to, let's stay where we are QListWidgetItem *item = completionBox()->item(currentIndex); if (item) { completionBox()->setCurrentItem(item); item->setSelected(true); } } return true; } // special case of the initial selection, which is unfortunately a header. // Setting it to selected tricks KCompletionBox into not treating is special // and selecting making it current, instead of the one below. QListWidgetItem *item = completionBox()->item(currentIndex); if (item && itemIsHeader(item)) { completionBox()->setCurrentItem(item); item->setSelected(true); } } else if (event->type() == QEvent::KeyRelease && (keyEvent->key() == Qt::Key_Tab || keyEvent->key() == Qt::Key_Backtab)) { /// first, find the header of the current section QListWidgetItem *myHeader = nullptr; int myHeaderIndex = -1; const int iterationStep = keyEvent->key() == Qt::Key_Tab ? 1 : -1; int index = qMin(qMax(currentIndex - iterationStep, 0), completionBox()->count() - 1); while (index >= 0) { if (itemIsHeader(completionBox()->item(index))) { myHeader = completionBox()->item(index); myHeaderIndex = index; break; } index--; } Q_ASSERT(myHeader); // we should always be able to find a header // find the next header (searching backwards, for Qt::Key_Backtab) QListWidgetItem *nextHeader = nullptr; // when iterating forward, start at the currentindex, when backwards, // one up from our header, or at the end int j; if (keyEvent->key() == Qt::Key_Tab) { j = currentIndex; } else { index = myHeaderIndex; if (index == 0) { j = completionBox()->count() - 1; } else { j = (index - 1) % completionBox()->count(); } } while ((nextHeader = completionBox()->item(j)) && nextHeader != myHeader) { if (itemIsHeader(nextHeader)) { break; } j = (j + iterationStep) % completionBox()->count(); } if (nextHeader && nextHeader != myHeader) { QListWidgetItem *item = completionBox()->item(j + 1); if (item && !itemIsHeader(item)) { completionBox()->setCurrentItem(item); item->setSelected(true); } } return true; } } return KLineEdit::eventFilter(object, event); } void AddresseeLineEdit::setText(const QString &text) { const int cursorPos = cursorPosition(); KLineEdit::setText(text.trimmed()); setCursorPosition(cursorPos); } void AddresseeLineEdit::insert(const QString &t) { if (!d->smartPaste()) { KLineEdit::insert(t); return; } QString newText = t.trimmed(); if (newText.isEmpty()) { return; } newText = adaptPasteMails(newText); QString contents = text(); int pos = cursorPosition(); if (hasSelectedText()) { // Cut away the selection. int start_sel = selectionStart(); pos = start_sel; contents = contents.left(start_sel) + contents.mid(start_sel + selectedText().length()); } int eot = contents.length(); while ((eot > 0) && contents.at(eot - 1).isSpace()) { --eot; } if (eot == 0) { contents.clear(); } else if (pos >= eot) { if (contents.at(eot - 1) == QLatin1Char(',')) { --eot; } contents.truncate(eot); contents += QStringLiteral(", "); pos = eot + 2; } contents = contents.left(pos) + newText + contents.mid(pos); setText(contents); setModified(true); setCursorPosition(pos + newText.length()); } void AddresseeLineEdit::paste() { if (d->useCompletion()) { d->setSmartPaste(true); } KLineEdit::paste(); d->setSmartPaste(false); } void AddresseeLineEdit::keyPressEvent(QKeyEvent *event) { bool accept = false; const int key = event->key() | event->modifiers(); if (KStandardShortcut::shortcut(KStandardShortcut::SubstringCompletion).contains(key)) { // TODO: add LDAP substring lookup, when it becomes available in KPIM::LDAPSearch d->updateSearchString(); d->startSearches(); d->doCompletion(true); accept = true; } else if (KStandardShortcut::shortcut(KStandardShortcut::TextCompletion).contains(key)) { const int len = text().length(); if (len == cursorPosition()) { // at End? d->updateSearchString(); d->startSearches(); d->doCompletion(true); accept = true; } } const QString oldContent = text(); if (!accept) { KLineEdit::keyPressEvent(event); } // if the text didn't change (eg. because a cursor navigation key was pressed) // we don't need to trigger a new search if (oldContent == text()) { return; } if (event->isAccepted()) { d->updateSearchString(); QString searchString(d->searchString()); // LDAP does not know about our string manipulation, remove it if (d->searchExtended()) { searchString = d->searchString().mid(1); } } } QString AddresseeLineEditPrivate::searchString() const { return mSearchString; } void AddresseeLineEditPrivate::setSearchString(const QString &searchString) { mSearchString = searchString; } bool AddresseeLineEditPrivate::searchExtended() const { return mSearchExtended; } void AddresseeLineEditPrivate::setSearchExtended(bool searchExtended) { mSearchExtended = searchExtended; } void AddresseeLineEdit::callUserCancelled(const QString &str) { userCancelled(str); } void AddresseeLineEdit::callSetCompletedText(const QString &text, bool marked) { setCompletedText(text, marked); } void AddresseeLineEdit::callSetCompletedText(const QString &text) { setCompletedText(text); } void AddresseeLineEdit::callSetUserSelection(bool b) { setUserSelection(b); } void AddresseeLineEdit::mouseReleaseEvent(QMouseEvent *event) { // reimplemented from QLineEdit::mouseReleaseEvent() if (d->useCompletion() && QApplication::clipboard()->supportsSelection() && !isReadOnly() && event->button() == Qt::MiddleButton) { d->setSmartPaste(true); } KLineEdit::mouseReleaseEvent(event); d->setSmartPaste(false); } void AddresseeLineEditPrivate::updateSearchString() { mSearchString = q->text(); int n = -1; bool inQuote = false; const int searchStringLength = mSearchString.length(); for (int i = 0; i < searchStringLength; ++i) { const QChar searchChar = mSearchString.at(i); if (searchChar == QLatin1Char('"')) { inQuote = !inQuote; } if (searchChar == QLatin1Char('\\') && (i + 1) < searchStringLength && mSearchString.at(i + 1) == QLatin1Char('"')) { ++i; } if (inQuote) { continue; } if (i < searchStringLength && (searchChar == QLatin1Char(',') || (mUseSemicolonAsSeparator && searchChar == QLatin1Char(';')))) { n = i; } } if (n >= 0) { ++n; // Go past the "," const int len = mSearchString.length(); // Increment past any whitespace... while (n < len && mSearchString.at(n).isSpace()) { ++n; } mPreviousAddresses = mSearchString.left(n); mSearchString = mSearchString.mid(n).trimmed(); } else { mPreviousAddresses.clear(); } } void AddresseeLineEditPrivate::startSearches() { if (!mDelayedQueryTimer->isActive()) { mDelayedQueryTimer->start(50ms); } } void AddresseeLineEditPrivate::slotCompletion() { // Called by KLineEdit's keyPressEvent for CompletionModes // Auto,Popup -> new text, update search string. // not called for CompletionShell, this is been taken care of // in AddresseeLineEdit::keyPressEvent updateSearchString(); if (q->completionBox()) { q->completionBox()->setCancelledText(mSearchString); } startSearches(); doCompletion(false); } void AddresseeLineEditPrivate::slotTriggerDelayedQueries() { const QString strSearch = mSearchString.trimmed(); if (strSearch.size() <= 2) { return; } searchInGnupg(); } void AddresseeLineEditPrivate::searchInGnupg() { const auto keys = Kleo::KeyCache::instance()->keys(); const QString trimmedString = mSearchString.trimmed(); for (const auto &key : keys) { for (int i = 0, count = key.numUserIDs(); i < count; i++) { auto email = QString::fromLatin1(key.userID(i).email()); if (email.startsWith(u'<')) { email.remove(0, 1); } if (email.endsWith(u'>')) { email.chop(1); } const auto name = QLatin1StringView(key.userID(i).name()); if (email.contains(trimmedString) || name.contains(trimmedString)) { if (name.trimmed().isEmpty()) { addCompletionItem(email, 1, 0); } else { addCompletionItem(name + u" <"_s + email + u'>', 1, 0); } } } } } void AddresseeLineEdit::slotEditingFinished() { - //const QList listJob = d->mightBeGroupJobs(); - //for (KJob *job : listJob) { - // disconnect(job); - // job->deleteLater(); - //} - - //d->mightBeGroupJobsClear(); - //d->groupsClear(); - - //if (!text().trimmed().isEmpty() && enableAkonadiSearch()) { - // const QStringList addresses = KEmailAddress::splitAddressList(text()); - // for (const QString &address : addresses) { - // auto job = new Akonadi::ContactGroupSearchJob(); - // connect(job, &Akonadi::ContactGroupSearchJob::result, this, &AddresseeLineEdit::slotGroupSearchResult); - // d->mightBeGroupJobsAdd(job); - // job->setQuery(Akonadi::ContactGroupSearchJob::Name, address); - // } - //} + // const QList listJob = d->mightBeGroupJobs(); + // for (KJob *job : listJob) { + // disconnect(job); + // job->deleteLater(); + // } + + // d->mightBeGroupJobsClear(); + // d->groupsClear(); + + // if (!text().trimmed().isEmpty() && enableAkonadiSearch()) { + // const QStringList addresses = KEmailAddress::splitAddressList(text()); + // for (const QString &address : addresses) { + // auto job = new Akonadi::ContactGroupSearchJob(); + // connect(job, &Akonadi::ContactGroupSearchJob::result, this, &AddresseeLineEdit::slotGroupSearchResult); + // d->mightBeGroupJobsAdd(job); + // job->setQuery(Akonadi::ContactGroupSearchJob::Name, address); + // } + // } } void AddresseeLineEdit::emitTextCompleted() { Q_EMIT textCompleted(); } #include "addresseelineedit.moc" diff --git a/client/editor/addresseelineeditmanager.cpp b/client/editor/addresseelineeditmanager.cpp index d3caa6a..6533113 100644 --- a/client/editor/addresseelineeditmanager.cpp +++ b/client/editor/addresseelineeditmanager.cpp @@ -1,98 +1,98 @@ /* SPDX-FileCopyrightText: 2015-2024 Laurent Montel SPDX-License-Identifier: GPL-2.0-or-later */ #include "addresseelineeditmanager.h" #include "kmailcompletion.h" #include "editor_debug.h" #include #include #include #include #include #include Q_GLOBAL_STATIC(AddresseeLineEditManager, sInstance) AddresseeLineEditManager::AddresseeLineEditManager() : mCompletion(std::make_unique()) - //, mAddresseeLineEditGnupg(new AddresseeLineEditGnupg) +//, mAddresseeLineEditGnupg(new AddresseeLineEditGnupg) { KConfigGroup group(KSharedConfig::openConfig(), QStringLiteral("AddressLineEdit")); mAutoGroupExpand = group.readEntry("AutoGroupExpand", false); } AddresseeLineEditManager::~AddresseeLineEditManager() = default; AddresseeLineEditManager *AddresseeLineEditManager::self() { return sInstance; } int AddresseeLineEditManager::addCompletionSource(const QString &source, int weight) { QMap::iterator it = completionSourceWeights.find(source); if (it == completionSourceWeights.end()) { completionSourceWeights.insert(source, weight); } else { completionSourceWeights[source] = weight; } const int sourceIndex = completionSources.indexOf(source); if (sourceIndex == -1) { completionSources.append(source); return completionSources.size() - 1; } else { return sourceIndex; } } void AddresseeLineEditManager::removeCompletionSource(const QString &source) { QMap::iterator it = completionSourceWeights.find(source); if (it != completionSourceWeights.end()) { completionSourceWeights.remove(source); mCompletion->clear(); } } KMailCompletion *AddresseeLineEditManager::completion() const { return mCompletion.get(); } bool AddresseeLineEditManager::isOnline() const { if (QNetworkInformation::loadBackendByFeatures(QNetworkInformation::Feature::Reachability)) { return QNetworkInformation::instance()->reachability() == QNetworkInformation::Reachability::Online; } else { qCWarning(EDITOR_LOG) << "Couldn't find a working backend for QNetworkInformation"; return false; } } bool AddresseeLineEditManager::autoGroupExpand() const { return mAutoGroupExpand; } void AddresseeLineEditManager::setAutoGroupExpand(bool checked) { if (mAutoGroupExpand != checked) { mAutoGroupExpand = checked; KConfigGroup group(KSharedConfig::openConfig(), QStringLiteral("AddressLineEdit")); group.writeEntry("AutoGroupExpand", mAutoGroupExpand); } } QColor AddresseeLineEditManager::alternateColor() const { if (!mAlternateColor.isValid()) { const KColorScheme colorScheme(QPalette::Active, KColorScheme::View); mAlternateColor = colorScheme.background(KColorScheme::AlternateBackground).color(); } return mAlternateColor; } diff --git a/client/editor/attachment/attachmentcompressjob.h b/client/editor/attachment/attachmentcompressjob.h index ca4b65a..971dfc8 100644 --- a/client/editor/attachment/attachmentcompressjob.h +++ b/client/editor/attachment/attachmentcompressjob.h @@ -1,73 +1,72 @@ /* SPDX-FileCopyrightText: 2009 Constantin Berzan SPDX-License-Identifier: LGPL-2.0-or-later */ #pragma once - #include "attachmentpart.h" #include #include namespace MessageCore { /** * @short A job to compress the attachment of an email. * * @author Constantin Berzan */ class AttachmentCompressJob : public KJob { Q_OBJECT public: /** * Creates a new attachment compress job. * * @param part The part of the attachment to compress. * @param parent The parent object. */ explicit AttachmentCompressJob(const AttachmentPart::Ptr &part, QObject *parent = nullptr); /** * Destroys the attachment compress job. */ ~AttachmentCompressJob() override; /** * Starts the attachment compress job. */ void start() override; /** * Sets the original @p part of the compressed attachment. */ void setOriginalPart(const AttachmentPart::Ptr &part); /** * Returns the original part of the compressed attachment. */ [[nodiscard]] const AttachmentPart::Ptr originalPart() const; /** * Returns the compressed part of the attachment. * * @note does not delete it unless it failed... */ [[nodiscard]] AttachmentPart::Ptr compressedPart() const; /** * Returns whether the compressed part is larger than the original part. */ [[nodiscard]] bool isCompressedPartLarger() const; private: //@cond PRIVATE class AttachmentCompressJobPrivate; std::unique_ptr const d; //@endcond }; } diff --git a/client/editor/attachment/attachmentcontroller.cpp b/client/editor/attachment/attachmentcontroller.cpp index 6440557..903f086 100644 --- a/client/editor/attachment/attachmentcontroller.cpp +++ b/client/editor/attachment/attachmentcontroller.cpp @@ -1,127 +1,126 @@ /* * This file is part of KMail. * SPDX-FileCopyrightText: 2009 Constantin Berzan * * Parts based on KMail code by: * Various authors. * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "attachmentcontroller.h" -#include "attachmentview.h" #include "attachmentmodel.h" #include "attachmentpart.h" #include "attachmentview.h" #include "editor/composerwindow.h" #include "editor_debug.h" -#include -#include #include "identity/identity.h" -#include +#include +#include #include +#include #include using namespace MessageCore; AttachmentController::AttachmentController(MessageComposer::AttachmentModel *model, AttachmentView *view, ComposerWindow *composer) : AttachmentControllerBase(model, composer, composer->actionCollection()) , mComposer(composer) , mView(view) { connect(composer, &ComposerWindow::identityChanged, this, &AttachmentController::identityChanged); connect(view, &AttachmentView::contextMenuRequested, this, &AttachmentControllerBase::showContextMenu); connect(view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &AttachmentController::selectionChanged); connect(view, &QAbstractItemView::doubleClicked, this, &AttachmentController::doubleClicked); connect(this, &AttachmentController::refreshSelection, this, &AttachmentController::selectionChanged); connect(this, &AttachmentController::showAttachment, this, &AttachmentController::onShowAttachment); connect(this, &AttachmentController::selectedAllAttachment, this, &AttachmentController::slotSelectAllAttachment); connect(this, &AttachmentController::actionsCreated, this, &AttachmentController::slotActionsCreated); } AttachmentController::~AttachmentController() = default; void AttachmentController::slotSelectAllAttachment() { mView->selectAll(); } void AttachmentController::identityChanged() { const KIdentityManagementCore::Identity &identity = mComposer->identity(); // "Attach public key" is only possible if OpenPGP support is available: enableAttachPublicKey(QGpgME::openpgp()); // "Attach my public key" is only possible if OpenPGP support is // available and the user specified his key for the current identity: enableAttachMyPublicKey(QGpgME::openpgp() && !identity.pgpEncryptionKey().isEmpty()); } void AttachmentController::attachMyPublicKey() { const KIdentityManagementCore::Identity &identity = mComposer->identity(); qCDebug(EDITOR_LOG) << identity.identityName(); auto keyCache = Kleo::KeyCache::instance(); auto key = keyCache->findByFingerprint(identity.pgpEncryptionKey().data()); exportPublicKey(key); } void AttachmentController::slotActionsCreated() { // Disable public key actions if appropriate. identityChanged(); // Disable actions like 'Remove', since nothing is currently selected. selectionChanged(); } void AttachmentController::selectionChanged() { const QModelIndexList selectedRows = mView->selectionModel()->selectedRows(); AttachmentPart::List selectedParts; selectedParts.reserve(selectedRows.count()); for (const QModelIndex &index : selectedRows) { auto part = mView->model()->data(index, MessageComposer::AttachmentModel::AttachmentPartRole).value(); selectedParts.append(part); } setSelectedParts(selectedParts); } void AttachmentController::onShowAttachment(KMime::Content *content) { if (content->bodyAsMessage()) { KMime::Message::Ptr message(new KMime::Message); message->setContent(content->bodyAsMessage()->encodedContent()); message->parse(); - auto dialog = new MimeTreeParser::Widgets::MessageViewerDialog({ message }); + auto dialog = new MimeTreeParser::Widgets::MessageViewerDialog({message}); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->show(); } } void AttachmentController::doubleClicked(const QModelIndex &itemClicked) { if (!itemClicked.isValid()) { qCDebug(EDITOR_LOG) << "Received an invalid item clicked index"; return; } // The itemClicked index will contain the column information. But we want to retrieve // the AttachmentPart, so we must recreate the QModelIndex without the column information const QModelIndex &properItemClickedIndex = mView->model()->index(itemClicked.row(), 0); auto part = mView->model()->data(properItemClickedIndex, MessageComposer::AttachmentModel::AttachmentPartRole).value(); // We can't edit encapsulated messages, but we can view them. if (part->isMessageOrMessageCollection()) { viewAttachment(part); } } #include "moc_attachmentcontroller.cpp" diff --git a/client/editor/attachment/attachmentcontrollerbase.cpp b/client/editor/attachment/attachmentcontrollerbase.cpp index 7418ab0..692a90b 100644 --- a/client/editor/attachment/attachmentcontrollerbase.cpp +++ b/client/editor/attachment/attachmentcontrollerbase.cpp @@ -1,714 +1,713 @@ /* * This file is part of KMail. * SPDX-FileCopyrightText: 2009 Constantin Berzan * * Parts based on KMail code by: * Various authors. * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "attachmentcontrollerbase.h" -#include "../attachment/attachmentjob.h" -#include "../attachment/attachmentmodel.h" -#include "../attachment/attachmentupdatejob.h" +#include "../attachment/attachmentclipboardjob.h" +#include "../attachment/attachmentcompressjob.h" #include "../attachment/attachmentfrompublickeyjob.h" #include "../attachment/attachmentfromurlbasejob.h" -#include "../attachment/attachmentcompressjob.h" -#include "../attachment/attachmentclipboardjob.h" #include "../attachment/attachmentfromurlutils.h" +#include "../attachment/attachmentjob.h" +#include "../attachment/attachmentmodel.h" #include "../attachment/attachmentpropertiesdialog.h" +#include "../attachment/attachmentupdatejob.h" #include "../composer.h" #include "../part/globalpart.h" #include "editor_debug.h" +#include +#include +#include +#include #include #include -#include -#include -#include -#include #include +#include #include -#include -#include -#include #include -#include +#include +#include +#include #include #include +#include #include #include -#include #include #include #include - using namespace MessageComposer; using namespace MessageCore; class MessageComposer::AttachmentControllerBase::AttachmentControllerBasePrivate { public: AttachmentControllerBasePrivate(AttachmentControllerBase *qq); ~AttachmentControllerBasePrivate(); void attachmentRemoved(const AttachmentPart::Ptr &part); // slot void compressJobResult(KJob *job); // slot void loadJobResult(KJob *job); // slot void openSelectedAttachments(); // slot void viewSelectedAttachments(); // slot void removeSelectedAttachments(); // slot void saveSelectedAttachmentAs(); // slot void selectedAttachmentProperties(); // slot void attachPublicKeyJobResult(KJob *job); // slot void slotAttachmentContentCreated(KJob *job); // slot void addAttachmentPart(AttachmentPart::Ptr part); void attachClipBoardElement(KJob *job); void selectedAllAttachment(); void reloadAttachment(); void updateJobResult(KJob *); AttachmentPart::List selectedParts; AttachmentControllerBase *const q; MessageComposer::AttachmentModel *model = nullptr; QWidget *wParent = nullptr; KActionCollection *mActionCollection = nullptr; QAction *attachPublicKeyAction = nullptr; QAction *attachMyPublicKeyAction = nullptr; QAction *openContextAction = nullptr; QAction *viewContextAction = 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 *reloadAttachmentAction = nullptr; QAction *attachClipBoardAction = nullptr; // If part p is compressed, uncompressedParts[p] is the uncompressed part. QHash uncompressedParts; bool encryptEnabled = false; bool signEnabled = false; }; AttachmentControllerBase::AttachmentControllerBasePrivate::AttachmentControllerBasePrivate(AttachmentControllerBase *qq) : q(qq) { } AttachmentControllerBase::AttachmentControllerBasePrivate::~AttachmentControllerBasePrivate() = default; void AttachmentControllerBase::setSelectedParts(const AttachmentPart::List &selectedParts) { d->selectedParts = selectedParts; const int selectedCount = selectedParts.count(); d->openContextAction->setEnabled(selectedCount > 0); d->viewContextAction->setEnabled(selectedCount > 0); 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::AttachmentControllerBasePrivate::attachmentRemoved(const AttachmentPart::Ptr &part) { uncompressedParts.remove(part); } void AttachmentControllerBase::AttachmentControllerBasePrivate::compressJobResult(KJob *job) { if (job->error()) { KMessageBox::error(wParent, job->errorString(), i18nc("@title:window", "Failed to compress attachment")); return; } auto ajob = qobject_cast(job); Q_ASSERT(ajob); AttachmentPart::Ptr originalPart = ajob->originalPart(); AttachmentPart::Ptr compressedPart = ajob->compressedPart(); if (ajob->isCompressedPartLarger()) { const int result = KMessageBox::questionTwoActions(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::ButtonCode::PrimaryAction) { // The user has chosen to keep the uncompressed file. return; } } qCDebug(EDITOR_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(EDITOR_LOG) << "Compressed a zombie."; } } void AttachmentControllerBase::AttachmentControllerBasePrivate::loadJobResult(KJob *job) { if (job->error()) { KMessageBox::error(wParent, job->errorString(), i18n("Failed to attach file")); return; } auto ajob = qobject_cast(job); Q_ASSERT(ajob); AttachmentPart::Ptr part = ajob->attachmentPart(); q->addAttachment(part); } void AttachmentControllerBase::AttachmentControllerBasePrivate::openSelectedAttachments() { Q_ASSERT(selectedParts.count() >= 1); for (const AttachmentPart::Ptr &part : std::as_const(selectedParts)) { q->openAttachment(part); } } void AttachmentControllerBase::AttachmentControllerBasePrivate::viewSelectedAttachments() { Q_ASSERT(selectedParts.count() >= 1); for (const AttachmentPart::Ptr &part : std::as_const(selectedParts)) { q->viewAttachment(part); } } void AttachmentControllerBase::AttachmentControllerBasePrivate::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) { if (!model->removeAttachment(part)) { qCWarning(EDITOR_LOG) << "Impossible to remove attachment" << part->fileName(); } } } void AttachmentControllerBase::AttachmentControllerBasePrivate::saveSelectedAttachmentAs() { Q_ASSERT(selectedParts.count() == 1); q->saveAttachmentAs(selectedParts.constFirst()); } void AttachmentControllerBase::AttachmentControllerBasePrivate::selectedAttachmentProperties() { Q_ASSERT(selectedParts.count() == 1); q->attachmentProperties(selectedParts.constFirst()); } void AttachmentControllerBase::AttachmentControllerBasePrivate::reloadAttachment() { Q_ASSERT(selectedParts.count() == 1); auto ajob = new AttachmentUpdateJob(selectedParts.constFirst(), q); connect(ajob, &AttachmentUpdateJob::result, q, [this](KJob *job) { updateJobResult(job); }); ajob->start(); } void AttachmentControllerBase::AttachmentControllerBasePrivate::updateJobResult(KJob *job) { if (job->error()) { KMessageBox::error(wParent, job->errorString(), i18n("Failed to reload attachment")); return; } auto ajob = qobject_cast(job); Q_ASSERT(ajob); 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(EDITOR_LOG) << "Updated a zombie."; } } void AttachmentControllerBase::exportPublicKey(const GpgME::Key &key) { if (key.isNull() || !QGpgME::openpgp()) { qCWarning(EDITOR_LOG) << "Tried to export key with empty fingerprint, or no OpenPGP."; return; } auto ajob = new MessageComposer::AttachmentFromPublicKeyJob(key, this); connect(ajob, &AttachmentFromPublicKeyJob::result, this, [this](KJob *job) { d->attachPublicKeyJobResult(job); }); ajob->start(); } void AttachmentControllerBase::AttachmentControllerBasePrivate::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::error(wParent, job->errorString(), i18n("Failed to attach public key")); return; } Q_ASSERT(dynamic_cast(job)); auto ajob = static_cast(job); AttachmentPart::Ptr part = ajob->attachmentPart(); q->addAttachment(part); } void AttachmentControllerBase::AttachmentControllerBasePrivate::attachClipBoardElement(KJob *job) { if (job->error()) { qCDebug(EDITOR_LOG) << " Error during when get try to attach text from clipboard"; KMessageBox::error(wParent, job->errorString(), i18n("Failed to attach text from clipboard")); return; } auto ajob = static_cast(job); AttachmentPart::Ptr part = ajob->attachmentPart(); q->addAttachment(part); } static QTemporaryFile *dumpAttachmentToTempFile(const AttachmentPart::Ptr &part) // local { auto file = new QTemporaryFile; if (!file->open()) { qCCritical(EDITOR_LOG) << "Could not open tempfile" << file->fileName(); delete file; return nullptr; } if (file->write(part->data()) == -1) { qCCritical(EDITOR_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 AttachmentControllerBasePrivate(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() = default; 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->setPopupMode(QToolButton::DelayedPopup); 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->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->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->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->setDefaultShortcut(d->addAttachmentFileAction, QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_A)); 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); 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(EDITOR_LOG) << "Compressing part."; auto ajob = new AttachmentCompressJob(part, this); connect(ajob, &AttachmentCompressJob::result, this, [this](KJob *job) { d->compressJobResult(job); }); ajob->start(); } else { qCDebug(EDITOR_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; 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); } } menu.addAction(d->openContextAction); } 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::openAttachment(const AttachmentPart::Ptr &part) { QTemporaryFile *tempFile = dumpAttachmentToTempFile(part); if (!tempFile) { KMessageBox::error(d->wParent, i18n("KMail was unable to write the attachment to a temporary file."), i18nc("@title:window", "Unable to open attachment")); return; } tempFile->setPermissions(QFile::ReadUser); if (!QDesktopServices::openUrl(QUrl::fromLocalFile(tempFile->fileName()))) { qWarning() << "Unable to open" << QUrl::fromLocalFile(tempFile->fileName()); } tempFile->deleteLater(); } void AttachmentControllerBase::viewAttachment(const AttachmentPart::Ptr &part) { auto composer = new MessageComposer::Composer; composer->globalPart()->setFallbackCharsetEnabled(true); auto attachmentJob = new MessageComposer::AttachmentJob(part, composer); connect(attachmentJob, &AttachmentJob::result, this, [this](KJob *job) { d->slotAttachmentContentCreated(job); }); attachmentJob->start(); } void AttachmentControllerBase::AttachmentControllerBasePrivate::slotAttachmentContentCreated(KJob *job) { if (!job->error()) { const MessageComposer::AttachmentJob *const attachmentJob = qobject_cast(job); Q_ASSERT(attachmentJob); if (attachmentJob) { Q_EMIT q->showAttachment(attachmentJob->content()); } } else { // TODO: show warning to the user qCWarning(EDITOR_LOG) << "Error creating KMime::Content for attachment:" << job->errorText(); } } void AttachmentControllerBase::saveAttachmentAs(const AttachmentPart::Ptr &part) { QString pname = part->name(); if (pname.isEmpty()) { pname = i18n("unnamed"); } const QString fileName = QFileDialog::getSaveFileName(d->wParent, i18n("Save Attachment As"), pname); if (fileName.isEmpty()) { qCDebug(EDITOR_LOG) << "Save Attachment As dialog canceled."; return; } QSaveFile file(fileName); if (!file.open(QIODevice::WriteOnly)) { KMessageBox::error(d->wParent, i18n("File %1 could not be created.", fileName), i18n("Error saving message")); return; } file.write(part->data()); file.commit(); } 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::warningTwoActions(d->wParent, i18n("Do you really want to attach this directory \"%1\"?", url.toLocalFile()), i18nc("@title:window", "Attach directory"), KGuiItem(i18nc("@action:button", "Attach")), KStandardGuiItem::cancel()); if (rc == KMessageBox::ButtonCode::PrimaryAction) { addAttachment(url); } } void AttachmentControllerBase::showAttachClipBoard() { auto 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() { const auto urls = QFileDialog::getOpenFileUrls(d->wParent, i18nc("@title:window", "Attach File")); if (!urls.isEmpty()) { for (const auto &url : urls) { QMimeDatabase mimeDb; const auto mimeType = mimeDb.mimeTypeForUrl(url); if (mimeType.name() == QLatin1String("inode/directory")) { const int rc = KMessageBox::warningTwoActions(d->wParent, i18n("Do you really want to attach this directory \"%1\"?", url.toLocalFile()), i18nc("@title:window", "Attach directory"), KGuiItem(i18nc("@action:button", "Attach")), KStandardGuiItem::cancel()); if (rc == KMessageBox::ButtonCode::PrimaryAction) { addAttachment(url); } } else { addAttachment(url); } } } } 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::error(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->selectedKey()); } 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::clear() { const auto parts = d->model->attachments(); for (const auto &attachmentPart : parts) { if (!d->model->removeAttachment(attachmentPart)) { qCWarning(EDITOR_LOG) << "Impossible to remove attachment" << attachmentPart->fileName(); } } } #include "moc_attachmentcontrollerbase.cpp" diff --git a/client/editor/attachment/attachmentfrommimecontentjob.h b/client/editor/attachment/attachmentfrommimecontentjob.h index 303ce02..39b50b0 100644 --- a/client/editor/attachment/attachmentfrommimecontentjob.h +++ b/client/editor/attachment/attachmentfrommimecontentjob.h @@ -1,61 +1,60 @@ /* SPDX-FileCopyrightText: 2009 Constantin Berzan SPDX-License-Identifier: LGPL-2.0-or-later */ #pragma once - #include "attachmentloadjob.h" #include namespace KMime { class Content; } namespace MessageCore { /** * @short A job to load an attachment from a mime content. * * @author Constantin Berzan */ class AttachmentFromMimeContentJob : public AttachmentLoadJob { Q_OBJECT public: /** * Creates a new job. * * @param content The mime content to load the attachment from. * @param parent The parent object. */ explicit AttachmentFromMimeContentJob(const KMime::Content *content, QObject *parent = nullptr); /** * Destroys the job. */ ~AttachmentFromMimeContentJob() override; /** * Sets the mime @p content to load the attachment from. */ void setMimeContent(const KMime::Content *content); /** * Returns the mime content to load the attachment from. */ [[nodiscard]] const KMime::Content *mimeContent() const; protected Q_SLOTS: void doStart() override; private: //@cond PRIVATE class AttachmentFromMimeContentJobPrivate; std::unique_ptr const d; //@endcond }; } diff --git a/client/editor/attachment/attachmentfromurlbasejob.h b/client/editor/attachment/attachmentfromurlbasejob.h index 76ba60e..d988abc 100644 --- a/client/editor/attachment/attachmentfromurlbasejob.h +++ b/client/editor/attachment/attachmentfromurlbasejob.h @@ -1,55 +1,54 @@ /* SPDX-FileCopyrightText: 2011 Martin Bednár SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once - #include "attachmentloadjob.h" #include #include namespace MessageCore { /** * @brief The AttachmentFromUrlBaseJob class */ class AttachmentFromUrlBaseJob : public AttachmentLoadJob { Q_OBJECT public: explicit AttachmentFromUrlBaseJob(const QUrl &url = QUrl(), QObject *parent = nullptr); ~AttachmentFromUrlBaseJob() override; /** * Returns the url that will be loaded as attachment. */ [[nodiscard]] QUrl url() const; /** * Returns the maximum size the attachment is allowed to have. */ [[nodiscard]] qint64 maximumAllowedSize() const; /** * Sets the @p url of the folder that will be loaded as attachment. */ void setUrl(const QUrl &url); /** * Sets the maximum @p size the attachment is allowed to have. */ void setMaximumAllowedSize(qint64 size); protected Q_SLOTS: void doStart() override = 0; private: //@cond PRIVATE class AttachmentFromUrlBaseJobPrivate; std::unique_ptr const d; }; } diff --git a/client/editor/attachment/attachmentfromurljob.cpp b/client/editor/attachment/attachmentfromurljob.cpp index 567497f..4e52d8c 100644 --- a/client/editor/attachment/attachmentfromurljob.cpp +++ b/client/editor/attachment/attachmentfromurljob.cpp @@ -1,153 +1,153 @@ /* SPDX-FileCopyrightText: 2009 Constantin Berzan Parts based on KMail code by various authors. SPDX-License-Identifier: LGPL-2.0-or-later */ #include "attachmentfromurljob.h" #include "editor_debug.h" -#include #include +#include #include #include -#include #include #include +#include using namespace MessageCore; class MessageCore::AttachmentFromUrlJob::AttachmentLoadJobPrivate { public: explicit AttachmentLoadJobPrivate(AttachmentFromUrlJob *qq); void slotRequestFinished(QNetworkReply *reply); AttachmentFromUrlJob *const q; QByteArray mData; }; AttachmentFromUrlJob::AttachmentLoadJobPrivate::AttachmentLoadJobPrivate(AttachmentFromUrlJob *qq) : q(qq) { } void AttachmentFromUrlJob::AttachmentLoadJobPrivate::slotRequestFinished(QNetworkReply *reply) { if (reply->error() != QNetworkReply::NoError) { // TODO this loses useful stuff from KIO, like detailed error descriptions, causes+solutions, // ... use UiDelegate somehow? q->setError(reply->error()); q->setErrorText(reply->errorString()); q->emitResult(); return; } mData = reply->readAll(); // Determine the MIME type and filename of the attachment. const QString mimeTypeName = reply->header(QNetworkRequest::ContentTypeHeader).toString(); qCDebug(EDITOR_LOG) << "Mimetype is" << mimeTypeName; QString fileName = q->url().fileName(); fileName.replace(QLatin1Char('\n'), QLatin1Char('_')); if (fileName.isEmpty()) { QMimeDatabase db; const auto mimeType = db.mimeTypeForName(mimeTypeName); if (mimeType.isValid()) { fileName = i18nc("a file called 'unknown.ext'", "unknown%1", mimeType.preferredSuffix()); } else { fileName = i18nc("a file called 'unknown'", "unknown"); } } // Create the AttachmentPart. Q_ASSERT(q->attachmentPart() == nullptr); // Not created before. AttachmentPart::Ptr part = AttachmentPart::Ptr(new AttachmentPart); part->setMimeType(mimeTypeName.toLatin1()); part->setName(fileName); part->setFileName(fileName); part->setData(mData); part->setUrl(q->url()); q->setAttachmentPart(part); q->emitResult(); // Success. } AttachmentFromUrlJob::AttachmentFromUrlJob(const QUrl &url, QObject *parent) : AttachmentFromUrlBaseJob(url, parent) , d(new AttachmentLoadJobPrivate(this)) { } AttachmentFromUrlJob::~AttachmentFromUrlJob() = default; void AttachmentFromUrlJob::doStart() { if (!url().isValid()) { setError(KJob::UserDefinedError); setErrorText(i18n("\"%1\" not found. Please specify the full path.", url().toDisplayString())); emitResult(); return; } if (maximumAllowedSize() != -1 && url().isLocalFile()) { const qint64 size = QFileInfo(url().toLocalFile()).size(); if (size > maximumAllowedSize()) { setError(KJob::UserDefinedError); KFormat format; setErrorText(i18n("You may not attach files bigger than %1. Share it with storage service.", format.formatByteSize(maximumAllowedSize()))); emitResult(); return; } } Q_ASSERT(d->mData.isEmpty()); // Not started twice. if (url().scheme() == QLatin1StringView("https") || url().scheme() == QLatin1StringView("http")) { auto qnam = new QNetworkAccessManager(this); QNetworkRequest request(url()); auto reply = qnam->get(request); connect(reply, &QNetworkReply::finished, this, [this, reply]() { d->slotRequestFinished(reply); }); } else if (url().scheme() == QLatin1StringView("file")) { QFile file(url().toLocalFile()); if (!file.open(QIODeviceBase::ReadOnly)) { setError(KJob::UserDefinedError); setErrorText(i18n("Could not read file \"%1\".", url().toDisplayString())); emitResult(); return; } QString fileName = url().fileName(); QMimeDatabase db; const auto mimeType = db.mimeTypeForFile(url().toLocalFile()); if (mimeType.isValid()) { fileName = i18nc("a file called 'unknown.ext'", "unknown%1", mimeType.preferredSuffix()); } else { fileName = i18nc("a file called 'unknown'", "unknown"); } // Create the AttachmentPart. Q_ASSERT(attachmentPart() == nullptr); // Not created before. AttachmentPart::Ptr part = AttachmentPart::Ptr(new AttachmentPart); part->setMimeType(mimeType.name().toUtf8()); part->setName(fileName); part->setFileName(fileName); part->setData(file.readAll()); part->setUrl(url()); setAttachmentPart(part); emitResult(); // Success. return; } setError(KJob::UserDefinedError); setErrorText(i18n("scheme not supported \"%1\".", url().scheme())); emitResult(); } #include "moc_attachmentfromurljob.cpp" diff --git a/client/editor/attachment/attachmentpart.h b/client/editor/attachment/attachmentpart.h index 079026a..3d0974a 100644 --- a/client/editor/attachment/attachmentpart.h +++ b/client/editor/attachment/attachmentpart.h @@ -1,197 +1,196 @@ /* SPDX-FileCopyrightText: 2009 Constantin Berzan SPDX-License-Identifier: LGPL-2.0-or-later */ #pragma once - #include #include #include #include class QUrl; namespace MessageCore { /** * @short A class that encapsulates an attachment. * * @author Constantin Berzan */ class AttachmentPart { public: /** * Defines a pointer to an attachment object. */ using Ptr = QSharedPointer; /** * Defines a list of pointers to attachment objects. */ using List = QList; /** * Creates a new attachment part. */ AttachmentPart(); /** * Destroys the attachment part. */ virtual ~AttachmentPart(); /** * Sets the @p name of the attachment. * * The name will be used in the 'name=' part of * the Content-Type header. */ void setName(const QString &name); /** * Returns the name of the attachment. */ [[nodiscard]] QString name() const; /** * Sets the file @p name of the attachment. * * The name will be used in the 'filename=' part of * the Content-Disposition header. */ void setFileName(const QString &name); /** * Returns the file name of the attachment. */ [[nodiscard]] QString fileName() const; /** * Sets the @p description of the attachment. */ void setDescription(const QString &description); /** * Returns the description of the attachment. */ [[nodiscard]] QString description() const; /** * Sets whether the attachment will be displayed inline the message. */ void setInline(bool value); /** * Returns whether the attachment will be displayed inline the message. */ [[nodiscard]] bool isInline() const; /** * Sets whether encoding of the attachment will be determined automatically. */ void setAutoEncoding(bool enabled); /** * Returns whether encoding of the attachment will be determined automatically. */ [[nodiscard]] bool isAutoEncoding() const; /** * Sets the @p encoding that will be used for the attachment. * * @note only applies if isAutoEncoding is @c false */ void setEncoding(KMime::Headers::contentEncoding encoding); /** * Returns the encoding that will be used for the attachment. */ [[nodiscard]] KMime::Headers::contentEncoding encoding() const; /** * Sets the @p charset that will be used for the attachment. */ void setCharset(const QByteArray &charset); /** * Returns the charset that will be used for the attachment. */ [[nodiscard]] QByteArray charset() const; /** * Sets the @p mimeType of the attachment. */ void setMimeType(const QByteArray &mimeType); /** * Returns the mime type of the attachment. */ [[nodiscard]] QByteArray mimeType() const; /** * Sets whether the attachment is @p compressed. */ void setCompressed(bool compressed); /** * Returns whether the attachment is compressed. */ [[nodiscard]] bool isCompressed() const; /** * Sets whether the attachment is @p encrypted. */ void setEncrypted(bool encrypted); /** * Returns whether the attachment is encrypted. */ [[nodiscard]] bool isEncrypted() const; /** * Sets whether the attachment is @p signed. */ void setSigned(bool sign); /** * Returns whether the attachment is signed. */ [[nodiscard]] bool isSigned() const; /** * Sets the payload @p data of the attachment. */ void setData(const QByteArray &data); /** * Returns the payload data of the attachment. */ [[nodiscard]] QByteArray data() const; /** * Returns the size of the attachment. */ [[nodiscard]] qint64 size() const; /** * Returns whether the specified attachment part is an encapsulated message * (message/rfc822) or a collection of encapsulated messages (multipart/digest) */ [[nodiscard]] bool isMessageOrMessageCollection() const; void setUrl(const QUrl &url); [[nodiscard]] QUrl url() const; private: //@cond PRIVATE class AttachmentPartPrivate; AttachmentPartPrivate *const d; //@endcond }; } Q_DECLARE_METATYPE(MessageCore::AttachmentPart::Ptr) diff --git a/client/editor/attachment/attachmentpropertiesdialog.h b/client/editor/attachment/attachmentpropertiesdialog.h index ad425a4..efe8753 100644 --- a/client/editor/attachment/attachmentpropertiesdialog.h +++ b/client/editor/attachment/attachmentpropertiesdialog.h @@ -1,89 +1,88 @@ /* SPDX-FileCopyrightText: 2009 Constantin Berzan Based on KMail code by various authors (kmmsgpartdlg). SPDX-License-Identifier: LGPL-2.0-or-later */ #pragma once - #include "attachmentpart.h" #include namespace MessageCore { /** * @short A dialog for editing attachment properties. * * @author Constantin Berzan */ class AttachmentPropertiesDialog : public QDialog { Q_OBJECT public: /** * Creates a new attachment properties dialog. * * @param part The attachment part which properties to change. * @param readOnly Whether the dialog should be in read-only mode. * @param parent The parent object. */ explicit AttachmentPropertiesDialog(const AttachmentPart::Ptr &part, bool readOnly = false, QWidget *parent = nullptr); /** * Creates a new attachment properties dialog. * * @param content The mime content that represents the attachment which properties to change. * @param parent The parent object. * * @note This converts the KMime::Content to an AttachmentPart internally. * Therefore, saving the changes to the KMime::Content is not supported, * and the dialog is in readOnly mode. */ explicit AttachmentPropertiesDialog(const KMime::Content *content, QWidget *parent = nullptr); /** * Destroys the attachment properties dialog. */ ~AttachmentPropertiesDialog() override; /** * Returns the modified attachment. */ [[nodiscard]] AttachmentPart::Ptr attachmentPart() const; /** * Sets whether the encryption status of the attachment can be changed. */ void setEncryptEnabled(bool enabled); /** * Returns whether the encryption status of the attachment can be changed. */ [[nodiscard]] bool isEncryptEnabled() const; /** * Sets whether the signature status of the attachment can be changed. */ void setSignEnabled(bool enabled); /** * Returns whether the signature status of the attachment can be changed. */ [[nodiscard]] bool isSignEnabled() const; public Q_SLOTS: void accept() override; void slotHelp(); private: //@cond PRIVATE class AttachmentPropertiesDialogPrivate; std::unique_ptr const d; //@endcond }; } diff --git a/client/editor/attachment/attachmentview.cpp b/client/editor/attachment/attachmentview.cpp index 90e6c64..7ed9c99 100644 --- a/client/editor/attachment/attachmentview.cpp +++ b/client/editor/attachment/attachmentview.cpp @@ -1,202 +1,202 @@ /* * This file is part of KMail. * SPDX-FileCopyrightText: 2011-2023 Laurent Montel * * SPDX-FileCopyrightText: 2009 Constantin Berzan * * Parts based on KMail code by: * SPDX-FileCopyrightText: 2003 Ingo Kloecker * SPDX-FileCopyrightText: 2007 Thomas McGuire * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "attachmentview.h" #include "attachmentmodel.h" #include "attachmentpart.h" #include #include #include #include +#include #include #include #include #include -#include +#include #include #include -#include using MessageCore::AttachmentPart; AttachmentView::AttachmentView(MessageComposer::AttachmentModel *model, QWidget *parent) : QTreeView(parent) , mModel(model) , mToolButton(new QToolButton(this)) , mInfoAttachment(new QLabel(this)) , mWidget(new QWidget()) , grp(KSharedConfig::openConfig()->group(QStringLiteral("AttachmentView"))) { auto lay = new QHBoxLayout(mWidget); lay->setContentsMargins({}); connect(mToolButton, &QAbstractButton::toggled, this, &AttachmentView::slotShowHideAttchementList); mToolButton->setIcon(QIcon::fromTheme(QStringLiteral("mail-attachment"))); mToolButton->setAutoRaise(true); mToolButton->setCheckable(true); lay->addWidget(mToolButton); mInfoAttachment->setContentsMargins({}); mInfoAttachment->setTextFormat(Qt::PlainText); lay->addWidget(mInfoAttachment); connect(model, &MessageComposer::AttachmentModel::encryptEnabled, this, &AttachmentView::setEncryptEnabled); connect(model, &MessageComposer::AttachmentModel::signEnabled, this, &AttachmentView::setSignEnabled); auto sortModel = new QSortFilterProxyModel(this); sortModel->setSortCaseSensitivity(Qt::CaseInsensitive); sortModel->setSourceModel(model); setModel(sortModel); connect(model, &MessageComposer::AttachmentModel::rowsInserted, this, &AttachmentView::hideIfEmpty); connect(model, &MessageComposer::AttachmentModel::rowsRemoved, this, &AttachmentView::hideIfEmpty); connect(model, &MessageComposer::AttachmentModel::rowsRemoved, this, &AttachmentView::selectNewAttachment); connect(model, &MessageComposer::AttachmentModel::dataChanged, this, &AttachmentView::updateAttachmentLabel); setRootIsDecorated(false); setUniformRowHeights(true); setSelectionMode(QAbstractItemView::ExtendedSelection); setDragDropMode(QAbstractItemView::DragDrop); setEditTriggers(QAbstractItemView::EditKeyPressed); setDropIndicatorShown(false); setSortingEnabled(true); header()->setSectionResizeMode(QHeaderView::Interactive); header()->setStretchLastSection(false); restoreHeaderState(); setColumnWidth(0, 200); } AttachmentView::~AttachmentView() { saveHeaderState(); } void AttachmentView::restoreHeaderState() { header()->restoreState(grp.readEntry("State", QByteArray())); } void AttachmentView::saveHeaderState() { grp.writeEntry("State", header()->saveState()); grp.sync(); } void AttachmentView::contextMenuEvent(QContextMenuEvent *event) { Q_UNUSED(event) Q_EMIT contextMenuRequested(); } void AttachmentView::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Delete) { // Indexes are based on row numbers, and row numbers change when items are deleted. // Therefore, first we need to make a list of AttachmentParts to delete. AttachmentPart::List toRemove; const QModelIndexList selectedIndexes = selectionModel()->selectedRows(); toRemove.reserve(selectedIndexes.count()); for (const QModelIndex &index : selectedIndexes) { auto part = model()->data(index, MessageComposer::AttachmentModel::AttachmentPartRole).value(); toRemove.append(part); } for (const AttachmentPart::Ptr &part : std::as_const(toRemove)) { mModel->removeAttachment(part); } } else { QTreeView::keyPressEvent(event); } } void AttachmentView::dragEnterEvent(QDragEnterEvent *event) { if (event->source() == this) { // Ignore drags from ourselves. event->ignore(); } else { QTreeView::dragEnterEvent(event); } } void AttachmentView::setEncryptEnabled(bool enabled) { setColumnHidden(MessageComposer::AttachmentModel::EncryptColumn, !enabled); } void AttachmentView::setSignEnabled(bool enabled) { setColumnHidden(MessageComposer::AttachmentModel::SignColumn, !enabled); } void AttachmentView::hideIfEmpty() { const bool needToShowIt = (model()->rowCount() > 0); setVisible(needToShowIt); mToolButton->setChecked(needToShowIt); widget()->setVisible(needToShowIt); if (needToShowIt) { updateAttachmentLabel(); } else { mInfoAttachment->clear(); } Q_EMIT modified(true); } void AttachmentView::updateAttachmentLabel() { const MessageCore::AttachmentPart::List list = mModel->attachments(); qint64 size = 0; for (const MessageCore::AttachmentPart::Ptr &part : list) { size += part->size(); } KFormat format; mInfoAttachment->setText(i18np("1 attachment (%2)", "%1 attachments (%2)", model()->rowCount(), format.formatByteSize(qMax(0LL, size)))); } void AttachmentView::selectNewAttachment() { if (selectionModel()->selectedRows().isEmpty()) { selectionModel()->select(selectionModel()->currentIndex(), QItemSelectionModel::Select | QItemSelectionModel::Rows); } } void AttachmentView::startDrag(Qt::DropActions supportedActions) { Q_UNUSED(supportedActions) const QModelIndexList selection = selectionModel()->selectedRows(); if (!selection.isEmpty()) { QMimeData *mimeData = model()->mimeData(selection); auto drag = new QDrag(this); drag->setMimeData(mimeData); drag->exec(Qt::CopyAction); } } QWidget *AttachmentView::widget() const { return mWidget; } void AttachmentView::slotShowHideAttchementList(bool show) { setVisible(show); if (show) { mToolButton->setToolTip(i18n("Hide attachment list")); } else { mToolButton->setToolTip(i18n("Show attachment list")); } } #include "moc_attachmentview.cpp" diff --git a/client/editor/composer.cpp b/client/editor/composer.cpp index 7c2fdc2..95dc8d1 100644 --- a/client/editor/composer.cpp +++ b/client/editor/composer.cpp @@ -1,686 +1,686 @@ /* SPDX-FileCopyrightText: 2009 Constantin Berzan SPDX-FileCopyrightText: 2009 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.net SPDX-FileCopyrightText: 2009 Leo Franchi SPDX-License-Identifier: LGPL-2.0-or-later */ #include "composer.h" #include "job/attachmentjob.h" #include "job/encryptjob.h" #include "job/itipjob.h" #include "job/jobbase_p.h" #include "job/maintextjob.h" #include "job/multipartjob.h" #include "job/signencryptjob.h" #include "job/signjob.h" #include "job/skeletonmessagejob.h" #include "job/transparentjob.h" #include "part/globalpart.h" #include "part/infopart.h" #include "part/itippart.h" #include "part/textpart.h" #include "editor_debug.h" #include using namespace MessageComposer; using MessageCore::AttachmentPart; class MessageComposer::ComposerPrivate : public JobBasePrivate { public: explicit ComposerPrivate(Composer *qq) : JobBasePrivate(qq) { } ~ComposerPrivate() override { delete skeletonMessage; } void init(); void doStart(); // slot void composeStep1(); void composeStep2(); [[nodiscard]] QList createEncryptJobs(ContentJobBase *contentJob, bool sign); void contentJobFinished(KJob *job); // slot void composeWithLateAttachments(KMime::Message *headers, KMime::Content *content, const AttachmentPart::List &parts, const std::vector &keys, const QStringList &recipients); void attachmentsFinished(KJob *job); // slot void composeFinalStep(KMime::Content *headers, KMime::Content *content); QString gnupgHome; QList>> encData; GpgME::Key senderEncryptionKey; std::vector signers; AttachmentPart::List attachmentParts; // attachments with different sign/encrypt settings from // main message body. added at the end of the process AttachmentPart::List lateAttachmentParts; QList resultMessages; Kleo::CryptoMessageFormat format; // Stuff that the application plays with. GlobalPart *globalPart = nullptr; InfoPart *infoPart = nullptr; TextPart *textPart = nullptr; ItipPart *itipPart = nullptr; // Stuff that we play with. KMime::Message *skeletonMessage = nullptr; bool started = false; bool finished = false; bool sign = false; bool encrypt = false; bool noCrypto = false; bool autoSaving = false; bool draft = false; Q_DECLARE_PUBLIC(Composer) }; void ComposerPrivate::init() { Q_Q(Composer); // We cannot create these in ComposerPrivate's constructor, because // their parent q is not fully constructed at that time. globalPart = new GlobalPart(q); infoPart = new InfoPart(q); textPart = nullptr; itipPart = nullptr; } void ComposerPrivate::doStart() { Q_ASSERT(!started); started = true; composeStep1(); } void ComposerPrivate::composeStep1() { Q_Q(Composer); // Create skeleton message (containing headers only; no content). auto skeletonJob = new SkeletonMessageJob(infoPart, globalPart, q); QObject::connect(skeletonJob, &SkeletonMessageJob::finished, q, [this, skeletonJob](KJob *job) { if (job->error()) { return; // KCompositeJob takes care of the error. } // SkeletonMessageJob is a special job creating a Message instead of a Content. Q_ASSERT(skeletonMessage == nullptr); skeletonMessage = skeletonJob->message(); Q_ASSERT(skeletonMessage); skeletonMessage->assemble(); composeStep2(); }); q->addSubjob(skeletonJob); skeletonJob->start(); } void ComposerPrivate::composeStep2() { Q_Q(Composer); ContentJobBase *mainJob = nullptr; ContentJobBase *mainContentJob = nullptr; Q_ASSERT(textPart || itipPart); // At least one must be present, otherwise it's a useless message if (textPart && !itipPart) { mainContentJob = new MainTextJob(textPart, q); } else if (!textPart && itipPart) { mainContentJob = new ItipJob(itipPart, q); } else { // Combination of both text and itip parts not supported right now Q_ASSERT(!textPart || !itipPart); } if ((sign || encrypt) && format & Kleo::InlineOpenPGPFormat) { // needs custom handling --- one SignEncryptJob by itself qCDebug(EDITOR_LOG) << "sending to sign/enc inline job!"; if (encrypt) { // TODO: fix Inline PGP with encrypted attachments const QList jobs = createEncryptJobs(mainContentJob, sign); for (ContentJobBase *subJob : jobs) { if (attachmentParts.isEmpty()) { // We have no attachments. Use the content given by the mainContentJob mainJob = subJob; } else { auto multipartJob = new MultipartJob(q); multipartJob->setMultipartSubtype("mixed"); multipartJob->appendSubjob(subJob); for (const AttachmentPart::Ptr &part : std::as_const(attachmentParts)) { multipartJob->appendSubjob(new AttachmentJob(part)); } mainJob = multipartJob; } QObject::connect(mainJob, SIGNAL(finished(KJob *)), q, SLOT(contentJobFinished(KJob *))); q->addSubjob(mainJob); } } else { auto subJob = new SignJob(q); subJob->setSigningKeys(signers); subJob->setCryptoMessageFormat(format); subJob->appendSubjob(mainContentJob); if (attachmentParts.isEmpty()) { // We have no attachments. Use the content given by the mainContentJob. mainJob = subJob; } else { auto multipartJob = new MultipartJob(q); multipartJob->setMultipartSubtype("mixed"); multipartJob->appendSubjob(subJob); for (const AttachmentPart::Ptr &part : std::as_const(attachmentParts)) { multipartJob->appendSubjob(new AttachmentJob(part)); } mainJob = multipartJob; } QObject::connect(mainJob, SIGNAL(finished(KJob *)), q, SLOT(contentJobFinished(KJob *))); q->addSubjob(mainJob); } if (mainJob) { mainJob->start(); } else { qCDebug(EDITOR_LOG) << "main job is null"; } return; } if (attachmentParts.isEmpty()) { // We have no attachments. Use the content given by the mainContentJob mainJob = mainContentJob; } else { // We have attachments. Create a multipart/mixed content. QMutableListIterator iter(attachmentParts); while (iter.hasNext()) { AttachmentPart::Ptr part = iter.next(); qCDebug(EDITOR_LOG) << "Checking attachment crypto policy... signed: " << part->isSigned() << " isEncrypted : " << part->isEncrypted(); if (!noCrypto && !autoSaving && !draft && (sign != part->isSigned() || encrypt != part->isEncrypted())) { // different policy qCDebug(EDITOR_LOG) << "got attachment with different crypto policy!"; lateAttachmentParts.append(part); iter.remove(); } } auto multipartJob = new MultipartJob(q); multipartJob->setMultipartSubtype("mixed"); multipartJob->appendSubjob(mainContentJob); for (const AttachmentPart::Ptr &part : std::as_const(attachmentParts)) { multipartJob->appendSubjob(new AttachmentJob(part)); } mainJob = multipartJob; } if (sign) { auto sJob = new SignJob(q); sJob->setCryptoMessageFormat(format); sJob->setSigningKeys(signers); sJob->appendSubjob(mainJob); sJob->setSkeletonMessage(skeletonMessage); mainJob = sJob; } if (encrypt) { const auto lstJob = createEncryptJobs(mainJob, false); for (ContentJobBase *job : lstJob) { auto eJob = dynamic_cast(job); if (eJob && sign) { // When doing Encrypt and Sign move headers only in the signed part eJob->setProtectedHeaders(false); } QObject::connect(job, SIGNAL(finished(KJob *)), q, SLOT(contentJobFinished(KJob *))); q->addSubjob(job); mainJob = job; // start only last EncryptJob } } else { QObject::connect(mainJob, SIGNAL(finished(KJob *)), q, SLOT(contentJobFinished(KJob *))); q->addSubjob(mainJob); } mainJob->start(); } QList ComposerPrivate::createEncryptJobs(ContentJobBase *contentJob, bool sign) { Q_Q(Composer); QList jobs; // each SplitInfo holds a list of recipients/keys, if there is more than // one item in it then it means there are secondary recipients that need // different messages w/ clean headers qCDebug(EDITOR_LOG) << "starting enc jobs"; qCDebug(EDITOR_LOG) << "format:" << format; qCDebug(EDITOR_LOG) << "enc data:" << encData.size(); if (encData.isEmpty()) { // no key data! bail! q->setErrorText(i18n("No key data for recipients found.")); q->setError(Composer::IncompleteError); q->emitResult(); return jobs; } const int encDataSize = encData.size(); jobs.reserve(encDataSize); for (int i = 0; i < encDataSize; ++i) { QPair> recipients = encData[i]; qCDebug(EDITOR_LOG) << "got first list of recipients:" << recipients.first; ContentJobBase *subJob = nullptr; if (sign) { auto seJob = new SignEncryptJob(q); seJob->setCryptoMessageFormat(format); seJob->setSigningKeys(signers); seJob->setEncryptionKeys(recipients.second); seJob->setRecipients(recipients.first); seJob->setSkeletonMessage(skeletonMessage); subJob = seJob; } else { auto eJob = new EncryptJob(q); eJob->setCryptoMessageFormat(format); eJob->setEncryptionKeys(recipients.second); eJob->setRecipients(recipients.first); eJob->setSkeletonMessage(skeletonMessage); eJob->setGnupgHome(gnupgHome); subJob = eJob; } qCDebug(EDITOR_LOG) << "subJob" << subJob; subJob->appendSubjob(contentJob); jobs.append(subJob); } qCDebug(EDITOR_LOG) << jobs.size(); return jobs; } void ComposerPrivate::contentJobFinished(KJob *job) { if (job->error()) { return; // KCompositeJob takes care of the error. } qCDebug(EDITOR_LOG) << "composing final message"; KMime::Message *headers = nullptr; KMime::Content *resultContent = nullptr; std::vector keys; QStringList recipients; Q_ASSERT(dynamic_cast(job) == static_cast(job)); auto contentJob = static_cast(job); // create the final headers and body, // taking into account secondary recipients for encryption if (encData.size() > 1) { // crypto job with secondary recipients.. Q_ASSERT(dynamic_cast(job)); // we need to get the recipients for this job auto eJob = dynamic_cast(job); keys = eJob->encryptionKeys(); recipients = eJob->recipients(); resultContent = contentJob->content(); // content() comes from superclass headers = new KMime::Message; headers->setHeader(skeletonMessage->from()); headers->setHeader(skeletonMessage->to()); headers->setHeader(skeletonMessage->cc()); headers->setHeader(skeletonMessage->subject()); headers->setHeader(skeletonMessage->date()); headers->setHeader(skeletonMessage->messageID()); auto realTo = new KMime::Headers::Generic("X-KMail-EncBccRecipients"); realTo->fromUnicodeString(eJob->recipients().join(QLatin1Char('%')), "utf-8"); qCDebug(EDITOR_LOG) << "got one of multiple messages sending to:" << realTo->asUnicodeString(); qCDebug(EDITOR_LOG) << "sending to recipients:" << recipients; headers->setHeader(realTo); headers->assemble(); } else { // just use the saved headers from before if (!encData.isEmpty()) { const auto firstElement = encData.at(0); qCDebug(EDITOR_LOG) << "setting enc data:" << firstElement.first << "with num keys:" << firstElement.second.size(); keys = firstElement.second; recipients = firstElement.first; } headers = skeletonMessage; resultContent = contentJob->content(); } if (lateAttachmentParts.isEmpty()) { composeFinalStep(headers, resultContent); } else { composeWithLateAttachments(headers, resultContent, lateAttachmentParts, keys, recipients); } } void ComposerPrivate::composeWithLateAttachments(KMime::Message *headers, KMime::Content *content, const AttachmentPart::List &parts, const std::vector &keys, const QStringList &recipients) { Q_Q(Composer); auto multiJob = new MultipartJob(q); multiJob->setMultipartSubtype("mixed"); // wrap the content into a job for the multijob to handle it auto tJob = new MessageComposer::TransparentJob(q); tJob->setContent(content); multiJob->appendSubjob(tJob); multiJob->setExtraContent(headers); qCDebug(EDITOR_LOG) << "attachment encr key size:" << keys.size() << " recipients: " << recipients; // operate correctly on each attachment that has a different crypto policy than body. for (const AttachmentPart::Ptr &attachment : std::as_const(parts)) { auto attachJob = new AttachmentJob(attachment, q); qCDebug(EDITOR_LOG) << "got a late attachment"; if (attachment->isSigned() && format) { qCDebug(EDITOR_LOG) << "adding signjob for late attachment"; auto sJob = new SignJob(q); sJob->setContent(nullptr); sJob->setCryptoMessageFormat(format); sJob->setSigningKeys(signers); sJob->appendSubjob(attachJob); if (attachment->isEncrypted()) { qCDebug(EDITOR_LOG) << "adding sign + encrypt job for late attachment"; auto eJob = new EncryptJob(q); eJob->setCryptoMessageFormat(format); eJob->setEncryptionKeys(keys); eJob->setRecipients(recipients); eJob->appendSubjob(sJob); multiJob->appendSubjob(eJob); } else { qCDebug(EDITOR_LOG) << "Just signing late attachment"; multiJob->appendSubjob(sJob); } } else if (attachment->isEncrypted() && format) { // only encryption qCDebug(EDITOR_LOG) << "just encrypting late attachment"; auto eJob = new EncryptJob(q); eJob->setCryptoMessageFormat(format); eJob->setEncryptionKeys(keys); eJob->setRecipients(recipients); eJob->appendSubjob(attachJob); multiJob->appendSubjob(eJob); } else { qCDebug(EDITOR_LOG) << "attaching plain non-crypto attachment"; auto attachSecondJob = new AttachmentJob(attachment, q); multiJob->appendSubjob(attachSecondJob); } } QObject::connect(multiJob, SIGNAL(finished(KJob *)), q, SLOT(attachmentsFinished(KJob *))); q->addSubjob(multiJob); multiJob->start(); } void ComposerPrivate::attachmentsFinished(KJob *job) { if (job->error()) { return; // KCompositeJob takes care of the error. } qCDebug(EDITOR_LOG) << "composing final message with late attachments"; Q_ASSERT(dynamic_cast(job)); auto contentJob = static_cast(job); KMime::Content *content = contentJob->content(); KMime::Content *headers = contentJob->extraContent(); composeFinalStep(headers, content); } void ComposerPrivate::composeFinalStep(KMime::Content *headers, KMime::Content *content) { content->assemble(); const QByteArray allData = headers->head() + content->encodedContent(); delete content; KMime::Message::Ptr resultMessage(new KMime::Message); resultMessage->setContent(allData); resultMessage->parse(); // Not strictly necessary. resultMessages.append(resultMessage); } Composer::Composer(QObject *parent) : JobBase(*new ComposerPrivate(this), parent) { Q_D(Composer); d->init(); } Composer::~Composer() = default; QList Composer::resultMessages() const { Q_D(const Composer); Q_ASSERT(d->finished); Q_ASSERT(!error()); return d->resultMessages; } GlobalPart *Composer::globalPart() const { Q_D(const Composer); return d->globalPart; } InfoPart *Composer::infoPart() const { Q_D(const Composer); return d->infoPart; } TextPart *Composer::textPart() const { Q_D(const Composer); if (!d->textPart) { auto *self = const_cast(this); self->d_func()->textPart = new TextPart(self); } return d->textPart; } void Composer::clearTextPart() { Q_D(Composer); delete d->textPart; d->textPart = nullptr; } ItipPart *Composer::itipPart() const { Q_D(const Composer); if (!d->itipPart) { auto *self = const_cast(this); self->d_func()->itipPart = new ItipPart(self); } return d->itipPart; } void Composer::clearItipPart() { Q_D(Composer); delete d->itipPart; d->itipPart = nullptr; } AttachmentPart::List Composer::attachmentParts() const { Q_D(const Composer); return d->attachmentParts; } void Composer::addAttachmentPart(AttachmentPart::Ptr part, bool autoresizeImage) { Q_D(Composer); Q_ASSERT(!d->started); Q_ASSERT(!d->attachmentParts.contains(part)); if (autoresizeImage) { - //MessageComposer::Utils resizeUtils; - //if (resizeUtils.resizeImage(part)) { - // MessageComposer::ImageScaling autoResizeJob; - // autoResizeJob.setName(part->name()); - // autoResizeJob.setMimetype(part->mimeType()); - // if (autoResizeJob.loadImageFromData(part->data())) { - // if (autoResizeJob.resizeImage()) { - // part->setData(autoResizeJob.imageArray()); - // part->setMimeType(autoResizeJob.mimetype()); - // part->setName(autoResizeJob.generateNewName()); - // resizeUtils.changeFileName(part); - // } - // } - //} + // MessageComposer::Utils resizeUtils; + // if (resizeUtils.resizeImage(part)) { + // MessageComposer::ImageScaling autoResizeJob; + // autoResizeJob.setName(part->name()); + // autoResizeJob.setMimetype(part->mimeType()); + // if (autoResizeJob.loadImageFromData(part->data())) { + // if (autoResizeJob.resizeImage()) { + // part->setData(autoResizeJob.imageArray()); + // part->setMimeType(autoResizeJob.mimetype()); + // part->setName(autoResizeJob.generateNewName()); + // resizeUtils.changeFileName(part); + // } + // } + // } } d->attachmentParts.append(part); } void Composer::addAttachmentParts(const AttachmentPart::List &parts, bool autoresizeImage) { for (const AttachmentPart::Ptr &part : parts) { addAttachmentPart(part, autoresizeImage); } } void Composer::removeAttachmentPart(AttachmentPart::Ptr part) { Q_D(Composer); Q_ASSERT(!d->started); const int numberOfElements = d->attachmentParts.removeAll(part); if (numberOfElements <= 0) { qCCritical(EDITOR_LOG) << "Unknown attachment part" << part.data(); Q_ASSERT(false); return; } } void Composer::setSignAndEncrypt(const bool doSign, const bool doEncrypt) { Q_D(Composer); d->sign = doSign; d->encrypt = doEncrypt; } void Composer::setCryptoMessageFormat(Kleo::CryptoMessageFormat format) { Q_D(Composer); d->format = format; } void Composer::setSigningKeys(const std::vector &signers) { Q_D(Composer); d->signers = signers; } void Composer::setEncryptionKeys(const QList>> &encData) { Q_D(Composer); d->encData = encData; } void Composer::setNoCrypto(bool noCrypto) { Q_D(Composer); d->noCrypto = noCrypto; } void Composer::setSenderEncryptionKey(const GpgME::Key &senderKey) { Q_D(Composer); d->senderEncryptionKey = senderKey; } void Composer::setGnupgHome(const QString &path) { Q_D(Composer); d->gnupgHome = path; } QString Composer::gnupgHome() const { Q_D(const Composer); return d->gnupgHome; } bool Composer::finished() const { Q_D(const Composer); return d->finished; } bool Composer::autoSave() const { Q_D(const Composer); return d->autoSaving; } void Composer::setAutoSave(bool isAutoSave) { Q_D(Composer); d->autoSaving = isAutoSave; } bool Composer::draft() const { Q_D(const Composer); return d->draft; } void Composer::setDraft(bool draft) { Q_D(Composer); d->draft = draft; } void Composer::start() { Q_D(Composer); d->doStart(); } void Composer::slotResult(KJob *job) { Q_D(Composer); JobBase::slotResult(job); if (!hasSubjobs()) { d->finished = true; emitResult(); } } #include "moc_composer.cpp" diff --git a/client/editor/composerviewbase.cpp b/client/editor/composerviewbase.cpp index 222b86f..1096ec6 100644 --- a/client/editor/composerviewbase.cpp +++ b/client/editor/composerviewbase.cpp @@ -1,1443 +1,1437 @@ /* SPDX-FileCopyrightText: 2010 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.com SPDX-FileCopyrightText: 2010 Leo Franchi SPDX-License-Identifier: LGPL-2.0-or-later */ #include "composerviewbase.h" +#include "../qnam.h" #include "attachment/attachmentcontrollerbase.h" #include "attachment/attachmentmodel.h" -#include "richtextcomposerng.h" -#include "richtextcomposersignatures.h" #include "composer.h" +#include "ews/ewsmailfactory.h" +#include "mailtemplates.h" #include "nodehelper.h" -#include "signaturecontroller.h" #include "part/globalpart.h" #include "part/infopart.h" +#include "richtextcomposerng.h" +#include "richtextcomposersignatures.h" +#include "signaturecontroller.h" #include "util.h" #include "util_p.h" -#include "ews/ewsmailfactory.h" -#include "mailtemplates.h" -#include "../qnam.h" #include "messagecomposersettings.h" #include "recipientseditor.h" -#include #include "identity/identity.h" -#include -#include +#include +#include #include #include -#include +#include +#include #include "editor_debug.h" #include -#include #include +#include +#include #include #include -#include #include #include #include #include #include #include #include #include #include +#include +#include #include #include #include #include -#include -#include #include #include "draft/draftmanager.h" using namespace MessageComposer; using namespace Qt::Literals::StringLiterals; ComposerViewBase::ComposerViewBase(QObject *parent, QWidget *parentGui) : QObject(parent) , m_msg(KMime::Message::Ptr(new KMime::Message)) , m_parentWidget(parentGui) , m_cryptoMessageFormat(Kleo::AutoFormat) , m_autoSaveInterval(60000) // default of 1 min { m_charsets << "utf-8"; // default, so we have a backup in case client code forgot to set. initAutoSave(); connect(this, &ComposerViewBase::composerCreated, this, &ComposerViewBase::slotComposerCreated); } ComposerViewBase::~ComposerViewBase() = default; bool ComposerViewBase::isComposing() const { return !m_composers.isEmpty(); } void ComposerViewBase::setMessage(const KMime::Message::Ptr &msg) { if (m_attachmentModel) { const auto attachments{m_attachmentModel->attachments()}; for (const MessageCore::AttachmentPart::Ptr &attachment : attachments) { if (!m_attachmentModel->removeAttachment(attachment)) { qCWarning(EDITOR_LOG) << "Attachment not found."; } } } m_msg = msg; if (m_recipientsEditor) { m_recipientsEditor->clear(); bool resultTooManyRecipients = m_recipientsEditor->setRecipientString(m_msg->to()->mailboxes(), Recipient::To); if (!resultTooManyRecipients) { resultTooManyRecipients = m_recipientsEditor->setRecipientString(m_msg->cc()->mailboxes(), Recipient::Cc); } if (!resultTooManyRecipients) { resultTooManyRecipients = m_recipientsEditor->setRecipientString(m_msg->bcc()->mailboxes(), Recipient::Bcc); } if (!resultTooManyRecipients) { resultTooManyRecipients = m_recipientsEditor->setRecipientString(m_msg->replyTo()->mailboxes(), Recipient::ReplyTo); } m_recipientsEditor->setFocusBottom(); Q_EMIT tooManyRecipient(resultTooManyRecipients); } // First, we copy the message and then parse it to the object tree parser. // The otp gets the message text out of it, in textualContent(), and also decrypts // the message if necessary. auto msgContent = new KMime::Content; msgContent->setContent(m_msg->encodedContent()); msgContent->parse(); // Load the attachments const auto attachments{msgContent->attachments()}; for (const auto &att : attachments) { addAttachmentPart(att); } // Set the HTML text and collect HTML images bool isHtml = false; const auto body = MailTemplates::body(msg, isHtml); if (isHtml) { Q_EMIT enableHtml(); } else { Q_EMIT disableHtml(LetUserConfirm); } editor()->setText(body); if (auto hdr = m_msg->headerByType("X-KMail-CursorPos")) { m_editor->setCursorPositionFromStart(hdr->asUnicodeString().toUInt()); } delete msgContent; } void ComposerViewBase::saveMailSettings() { auto header = new KMime::Headers::Generic("X-KMail-Identity"); header->fromUnicodeString(QString::number(m_identity.uoid()), "utf-8"); m_msg->setHeader(header); header = new KMime::Headers::Generic("X-KMail-Identity-Name"); header->fromUnicodeString(m_identity.identityName(), "utf-8"); m_msg->setHeader(header); header = new KMime::Headers::Generic("X-KMail-Dictionary"); header->fromUnicodeString(m_dictionary->currentDictionary(), "utf-8"); m_msg->setHeader(header); // Save the quote prefix which is used for this message. Each message can have // a different quote prefix, for example depending on the original sender. if (m_editor->quotePrefixName().isEmpty()) { m_msg->removeHeader("X-KMail-QuotePrefix"); } else { header = new KMime::Headers::Generic("X-KMail-QuotePrefix"); header->fromUnicodeString(m_editor->quotePrefixName(), "utf-8"); m_msg->setHeader(header); } if (m_editor->composerControler()->isFormattingUsed()) { qCDebug(EDITOR_LOG) << "HTML mode"; header = new KMime::Headers::Generic("X-KMail-Markup"); header->fromUnicodeString(QStringLiteral("true"), "utf-8"); m_msg->setHeader(header); } else { m_msg->removeHeader("X-KMail-Markup"); qCDebug(EDITOR_LOG) << "Plain text"; } } void ComposerViewBase::clearFollowUp() { mFollowUpDate = QDate(); } void ComposerViewBase::send() { KCursorSaver saver(Qt::WaitCursor); saveMailSettings(); if (m_editor->composerControler()->isFormattingUsed() && inlineSigningEncryptionSelected()) { const QString keepBtnText = m_encrypt ? m_sign ? i18n("&Keep markup, do not sign/encrypt") : i18n("&Keep markup, do not encrypt") : i18n("&Keep markup, do not sign"); const QString yesBtnText = m_encrypt ? m_sign ? i18n("Sign/Encrypt (delete markup)") : i18n("Encrypt (delete markup)") : i18n("Sign (delete markup)"); int ret = KMessageBox::warningTwoActionsCancel(m_parentWidget, i18n("

Inline signing/encrypting of HTML messages is not possible;

" "

do you want to delete your markup?

"), i18nc("@title:window", "Sign/Encrypt Message?"), KGuiItem(yesBtnText), KGuiItem(keepBtnText)); if (KMessageBox::Cancel == ret) { return; } if (KMessageBox::ButtonCode::SecondaryAction == ret) { m_encrypt = false; m_sign = false; } else { Q_EMIT disableHtml(NoConfirmationNeeded); } } readyForSending(); } void ComposerViewBase::setCustomHeader(const QMap &customHeader) { m_customHeader = customHeader; } void ComposerViewBase::readyForSending() { qCDebug(EDITOR_LOG) << "Entering readyForSending"; if (!m_msg) { qCDebug(EDITOR_LOG) << "m_msg == 0!"; return; } if (!m_composers.isEmpty()) { // This may happen if e.g. the autosave timer calls applyChanges. qCDebug(EDITOR_LOG) << "ready for sending: Called while composer active; ignoring. Number of composer " << m_composers.count(); return; } mExpandedFrom = from(); mExpandedTo = m_recipientsEditor->recipientStringList(Recipient::To); mExpandedCc = m_recipientsEditor->recipientStringList(Recipient::Cc); mExpandedBcc = m_recipientsEditor->recipientStringList(Recipient::Bcc); mExpandedReplyTo = m_recipientsEditor->recipientStringList(Recipient::ReplyTo); Q_ASSERT(m_composers.isEmpty()); // composers should be empty. The caller of this function // checks for emptiness before calling it // so just ensure it actually is empty // and document it // we first figure out if we need to create multiple messages with different crypto formats // if so, we create a composer per format // if we aren't signing or encrypting, this just returns a single empty message if (m_neverEncrypt) { auto composer = new MessageComposer::Composer; composer->setNoCrypto(true); m_composers.append(composer); slotComposerCreated(); } else { generateCryptoMessages(); } } void ComposerViewBase::slotComposerCreated() { if (m_composers.isEmpty()) { Q_EMIT failed(i18n("It was not possible to create a message composer.")); return; } // Compose each message and prepare it for queueing, sending, or storing // working copy in case composers instantly emit result const auto composers = m_composers; for (MessageComposer::Composer *composer : composers) { fillComposer(composer, UseExpandedRecipients, false); connect(composer, &MessageComposer::Composer::result, this, &ComposerViewBase::slotSendComposeResult); composer->start(); qCDebug(EDITOR_LOG) << "Started a composer for sending!"; } } namespace { // helper methods for reading encryption settings inline Kleo::chrono::days encryptOwnKeyNearExpiryWarningThresholdInDays() { if (!MessageComposer::MessageComposerSettings::self()->cryptoWarnWhenNearExpire()) { return Kleo::chrono::days{-1}; } const int num = MessageComposer::MessageComposerSettings::self()->cryptoWarnOwnEncrKeyNearExpiryThresholdDays(); return Kleo::chrono::days{qMax(1, num)}; } inline Kleo::chrono::days encryptKeyNearExpiryWarningThresholdInDays() { if (!MessageComposer::MessageComposerSettings::self()->cryptoWarnWhenNearExpire()) { return Kleo::chrono::days{-1}; } const int num = MessageComposer::MessageComposerSettings::self()->cryptoWarnEncrKeyNearExpiryThresholdDays(); return Kleo::chrono::days{qMax(1, num)}; } inline Kleo::chrono::days encryptRootCertNearExpiryWarningThresholdInDays() { if (!MessageComposer::MessageComposerSettings::self()->cryptoWarnWhenNearExpire()) { return Kleo::chrono::days{-1}; } const int num = MessageComposer::MessageComposerSettings::self()->cryptoWarnEncrRootNearExpiryThresholdDays(); return Kleo::chrono::days{qMax(1, num)}; } inline Kleo::chrono::days encryptChainCertNearExpiryWarningThresholdInDays() { if (!MessageComposer::MessageComposerSettings::self()->cryptoWarnWhenNearExpire()) { return Kleo::chrono::days{-1}; } const int num = MessageComposer::MessageComposerSettings::self()->cryptoWarnEncrChaincertNearExpiryThresholdDays(); return Kleo::chrono::days{qMax(1, num)}; } } // nameless namespace - Kleo::KeyResolver *ComposerViewBase::fillKeyResolver(bool encryptSomething) { auto keyResolverCore = new Kleo::KeyResolver(m_encrypt, m_sign, GpgME::UnknownProtocol, m_encrypt); keyResolverCore->setMinimumValidity(GpgME::UserID::Unknown); QStringList signingKeys, encryptionKeys; if (m_cryptoMessageFormat & Kleo::AnyOpenPGP) { if (!m_identity.pgpSigningKey().isEmpty()) { signingKeys.push_back(QLatin1String(m_identity.pgpSigningKey())); } if (!m_identity.pgpEncryptionKey().isEmpty()) { encryptionKeys.push_back(QLatin1String(m_identity.pgpEncryptionKey())); } } if (m_cryptoMessageFormat & Kleo::AnySMIME) { if (!m_identity.smimeSigningKey().isEmpty()) { signingKeys.push_back(QLatin1String(m_identity.smimeSigningKey())); } if (!m_identity.smimeEncryptionKey().isEmpty()) { encryptionKeys.push_back(QLatin1String(m_identity.smimeEncryptionKey())); } } keyResolverCore->setSender(m_identity.fullEmailAddr()); const auto normalized = GpgME::UserID::addrSpecFromString(m_identity.fullEmailAddr().toUtf8().constData()); const auto normalizedSender = QString::fromUtf8(normalized.c_str()); keyResolverCore->setSigningKeys(signingKeys); keyResolverCore->setOverrideKeys({{GpgME::UnknownProtocol, {{normalizedSender, encryptionKeys}}}}); if (encryptSomething) { QStringList recipients; const auto lst = m_recipientsEditor->lines(); for (auto line : lst) { auto recipient = line->data().dynamicCast(); recipients.push_back(recipient->email()); } keyResolverCore->setRecipients(recipients); } return keyResolverCore; } void ComposerViewBase::generateCryptoMessages() { bool canceled = false; qCDebug(EDITOR_LOG) << "filling crypto info"; connect(expiryChecker().get(), &Kleo::ExpiryChecker::expiryMessage, this, [&canceled](const GpgME::Key &key, QString msg, Kleo::ExpiryChecker::ExpiryInformation info, bool isNewMessage) { if (!isNewMessage) { return; } if (canceled) { return; } QString title; QString dontAskAgainName; if (info == Kleo::ExpiryChecker::OwnKeyExpired || info == Kleo::ExpiryChecker::OwnKeyNearExpiry) { dontAskAgainName = QStringLiteral("own key expires soon warning"); } else { dontAskAgainName = QStringLiteral("other encryption key near expiry warning"); } if (info == Kleo::ExpiryChecker::OwnKeyExpired || info == Kleo::ExpiryChecker::OtherKeyExpired) { title = key.protocol() == GpgME::OpenPGP ? i18n("OpenPGP Key Expired") : i18n("S/MIME Certificate Expired"); } else { title = key.protocol() == GpgME::OpenPGP ? i18n("OpenPGP Key Expires Soon") : i18n("S/MIME Certificate Expires Soon"); } if (KMessageBox::warningContinueCancel(nullptr, msg, title, KStandardGuiItem::cont(), KStandardGuiItem::cancel(), dontAskAgainName) == KMessageBox::Cancel) { canceled = true; } }); bool signSomething = m_sign; bool doSignCompletely = m_sign; bool encryptSomething = m_encrypt; bool doEncryptCompletely = m_encrypt; if (m_attachmentModel) { const auto attachments = m_attachmentModel->attachments(); for (const MessageCore::AttachmentPart::Ptr &attachment : attachments) { if (attachment->isSigned()) { signSomething = true; } else { doEncryptCompletely = false; } if (attachment->isEncrypted()) { encryptSomething = true; } else { doSignCompletely = false; } } } qCInfo(EDITOR_LOG) << "Encrypting completely:" << doEncryptCompletely; qCInfo(EDITOR_LOG) << "Signing completely:" << doSignCompletely; // No encryption or signing is needed if (!signSomething && !encryptSomething) { - m_composers = { new MessageComposer::Composer }; - Q_EMIT composerCreated(); - return; + m_composers = {new MessageComposer::Composer}; + Q_EMIT composerCreated(); + return; } auto keyResolver = fillKeyResolver(encryptSomething); keyResolver->start(true); connect(keyResolver, &Kleo::KeyResolver::keysResolved, this, [this, encryptSomething, signSomething, keyResolver](bool success, bool sendUnencrypted) { - if (!success) { qCDebug(EDITOR_LOG) << "resolveAllKeys: failed to resolve keys! oh noes"; Q_EMIT failed(i18n("Failed to resolve keys. Please report a bug.")); return; } qCDebug(EDITOR_LOG) << "done resolving keys."; if (sendUnencrypted) { - m_composers = { new MessageComposer::Composer }; + m_composers = {new MessageComposer::Composer}; Q_EMIT composerCreated(); return; } const auto result = keyResolver->result(); QList composers; auto signingKeyFinder = [&result](const GpgME::Protocol protocol) -> std::optional { // clazy:excludeall=lambda-in-connect for (const auto &key : result.signingKeys) { if (key.protocol() == protocol) { return key; } } return std::nullopt; }; if (encryptSomething || signSomething) { if (encryptSomething) { std::vector pgpKeys; QStringList pgpRecipients; std::vector smimeKeys; QStringList smimeRecipients; for (const auto &[recipient, keys] : result.encryptionKeys.asKeyValueRange()) { const auto recipientKeys = result.encryptionKeys[recipient]; if (recipientKeys.size() > 1) { // TODO Carl group handling } else { const auto &key = recipientKeys[0]; if (key.protocol() == GpgME::CMS) { smimeRecipients.append(recipient); smimeKeys.push_back(recipientKeys[0]); } else { pgpRecipients.append(recipient); pgpKeys.push_back(recipientKeys[0]); } } } Q_ASSERT(smimeRecipients.count() == (int)smimeKeys.size()); Q_ASSERT(pgpRecipients.count() == (int)pgpKeys.size()); if (pgpRecipients.count() > 0) { auto composer = new MessageComposer::Composer; - composer->setEncryptionKeys({ QPair>(pgpRecipients, pgpKeys) }); + composer->setEncryptionKeys({QPair>(pgpRecipients, pgpKeys)}); auto pgpSigningKey = signingKeyFinder(GpgME::OpenPGP); if (signSomething && pgpSigningKey) { - composer->setSigningKeys({ *pgpSigningKey }); + composer->setSigningKeys({*pgpSigningKey}); } composer->setCryptoMessageFormat(Kleo::OpenPGPMIMEFormat); composer->setSignAndEncrypt(signSomething, encryptSomething); composers << composer; } if (smimeRecipients.count() > 0) { auto composer = new MessageComposer::Composer; - composer->setEncryptionKeys({ QPair>(smimeRecipients, smimeKeys) }); + composer->setEncryptionKeys({QPair>(smimeRecipients, smimeKeys)}); auto smimeSigningKey = signingKeyFinder(GpgME::CMS); if (signSomething && smimeSigningKey) { - composer->setSigningKeys({ *smimeSigningKey }); + composer->setSigningKeys({*smimeSigningKey}); } composer->setCryptoMessageFormat(Kleo::SMIMEFormat); composer->setSignAndEncrypt(signSomething, encryptSomething); composers << composer; } } else { // signing only Q_ASSERT(signSomething); Q_ASSERT(!encryptSomething); auto composer = new MessageComposer::Composer; composer->setSignAndEncrypt(signSomething, encryptSomething); Q_ASSERT(result.protocol != GpgME::UnknownProtocol); // No mixed protocol allowed here const auto signingKey = signingKeyFinder(result.protocol); Q_ASSERT(signingKey); - composer->setSigningKeys({ *signingKey }); + composer->setSigningKeys({*signingKey}); qDebug() << result.protocol; composer->setCryptoMessageFormat(result.protocol == GpgME::OpenPGP ? Kleo::OpenPGPMIMEFormat : Kleo::SMIMEFormat); composers << composer; } } else { auto composer = new MessageComposer::Composer; composers.append(composer); // If we canceled sign or encrypt be sure to change status in attachment. markAllAttachmentsForSigning(false); markAllAttachmentsForEncryption(false); } if (composers.isEmpty() && (signSomething || encryptSomething)) { Q_ASSERT_X(false, "ComposerViewBase::generateCryptoMessages", "No concrete sign or encrypt method selected"); } m_composers = composers; Q_EMIT composerCreated(); }); } void ComposerViewBase::fillGlobalPart(MessageComposer::GlobalPart *globalPart) { globalPart->setParentWidgetForGui(m_parentWidget); globalPart->setCharsets(m_charsets); globalPart->setMDNRequested(m_mdnRequested); globalPart->setRequestDeleveryConfirmation(m_requestDeleveryConfirmation); } void ComposerViewBase::fillInfoPart(MessageComposer::InfoPart *infoPart, ComposerViewBase::RecipientExpansion expansion) { // TODO splitAddressList and expandAliases ugliness should be handled by a // special AddressListEdit widget... (later: see RecipientsEditor) if (expansion == UseExpandedRecipients) { infoPart->setFrom(mExpandedFrom); infoPart->setTo(mExpandedTo); infoPart->setCc(mExpandedCc); infoPart->setBcc(mExpandedBcc); infoPart->setReplyTo(mExpandedReplyTo); } else { infoPart->setFrom(from()); infoPart->setTo(m_recipientsEditor->recipientStringList(Recipient::To)); infoPart->setCc(m_recipientsEditor->recipientStringList(Recipient::Cc)); infoPart->setBcc(m_recipientsEditor->recipientStringList(Recipient::Bcc)); infoPart->setReplyTo(m_recipientsEditor->recipientStringList(Recipient::ReplyTo)); } infoPart->setSubject(subject()); infoPart->setUserAgent(QStringLiteral("KMail")); infoPart->setUrgent(m_urgent); if (auto inReplyTo = m_msg->inReplyTo(false)) { infoPart->setInReplyTo(inReplyTo->asUnicodeString()); } if (auto references = m_msg->references(false)) { infoPart->setReferences(references->asUnicodeString()); } KMime::Headers::Base::List extras; if (auto hdr = m_msg->headerByType("X-KMail-SignatureActionEnabled")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-EncryptActionEnabled")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-CryptoMessageFormat")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-UnExpanded-To")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-UnExpanded-CC")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-UnExpanded-BCC")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-UnExpanded-Reply-To")) { extras << hdr; } if (auto hdr = m_msg->organization(false)) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-Identity")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-Transport")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-Fcc")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-Drafts")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-Templates")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-Link-Message")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-Link-Type")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-Face")) { extras << hdr; } if (auto hdr = m_msg->headerByType("Face")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-FccDisabled")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-Identity-Name")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-Transport-Name")) { extras << hdr; } infoPart->setExtraHeaders(extras); } void ComposerViewBase::slotSendComposeResult(KJob *job) { Q_ASSERT(dynamic_cast(job)); auto composer = static_cast(job); if (composer->error() != MessageComposer::Composer::NoError) { qCDebug(EDITOR_LOG) << "compose job might have error: " << job->error() << " errorString: " << job->errorString(); } if (composer->error() == MessageComposer::Composer::NoError) { Q_ASSERT(m_composers.contains(composer)); // The messages were composed successfully. qCDebug(EDITOR_LOG) << "NoError."; const int numberOfMessage(composer->resultMessages().size()); for (int i = 0; i < numberOfMessage; ++i) { queueMessage(composer->resultMessages().at(i)); } } else if (composer->error() == MessageComposer::Composer::UserCancelledError) { // The job warned the user about something, and the user chose to return // to the message. Nothing to do. qCDebug(EDITOR_LOG) << "UserCancelledError."; Q_EMIT failed(i18n("Job cancelled by the user")); } else { qCDebug(EDITOR_LOG) << "other Error." << composer->error(); QString msg; if (composer->error() == MessageComposer::Composer::BugError) { msg = i18n("Could not compose message: %1 \n Please report this bug.", job->errorString()); } else { msg = i18n("Could not compose message: %1", job->errorString()); } Q_EMIT failed(msg); } if (!composer->gnupgHome().isEmpty()) { QDir dir(composer->gnupgHome()); dir.removeRecursively(); } m_composers.removeAll(composer); } void ComposerViewBase::setBearerToken(const QByteArray &bearerToken) { m_bearerToken = bearerToken; } void ComposerViewBase::queueMessage(const KMime::Message::Ptr &message) { - qWarning().noquote() << message->encodedContent(); auto soapRequestBody = EwsMailFactory::create(message); QNetworkRequest sendMailRequest(QUrl(u"https://127.0.0.1:5656/socket-web"_s)); sendMailRequest.setHeader(QNetworkRequest::ContentTypeHeader, u"application/xml"_s); sendMailRequest.setRawHeader("X-TOKEN", m_bearerToken); sendMailRequest.setRawHeader("X-EMAIL", from().toUtf8()); const QJsonDocument payload(QJsonObject{ - { "type"_L1, "ews"_L1 }, - { "payload"_L1, soapRequestBody }, - { "id"_L1, mailId() }, + {"type"_L1, "ews"_L1}, + {"payload"_L1, soapRequestBody}, + {"id"_L1, mailId()}, }); auto sendMailResponse = qnam->post(sendMailRequest, payload.toJson()); // TODO remove me QObject::connect(qnam, &QNetworkAccessManager::sslErrors, qnam, [](QNetworkReply *reply, const QList &errors) { Q_UNUSED(errors); reply->ignoreSslErrors(); }); connect(sendMailResponse, &QNetworkReply::finished, this, [this, sendMailResponse]() { qDebug() << sendMailResponse << sendMailResponse->error() << sendMailResponse->errorString(); if (sendMailResponse->error() != QNetworkReply::NoError) { Q_EMIT failed(i18nc("Error message", "There were a problem sending the message: %1", sendMailResponse->errorString())); return; } Q_EMIT sentSuccessfully(); }); qCDebug(EDITOR_LOG) << "Request body" << soapRequestBody; } void ComposerViewBase::initAutoSave() { qCDebug(EDITOR_LOG) << "initialising autosave"; // Ensure that the autosave directory exists. QDir dataDirectory(DraftManager::autosaveDirectory()); if (!dataDirectory.exists(QStringLiteral("autosave"))) { qCDebug(EDITOR_LOG) << "Creating autosave directory."; dataDirectory.mkdir(QStringLiteral("autosave")); } updateAutoSave(); } QDate ComposerViewBase::followUpDate() const { return mFollowUpDate; } void ComposerViewBase::setFollowUpDate(const QDate &followUpDate) { mFollowUpDate = followUpDate; } Sonnet::DictionaryComboBox *ComposerViewBase::dictionary() const { return m_dictionary; } void ComposerViewBase::setDictionary(Sonnet::DictionaryComboBox *dictionary) { m_dictionary = dictionary; } void ComposerViewBase::updateAutoSave() { if (m_autoSaveInterval == 0) { delete m_autoSaveTimer; m_autoSaveTimer = nullptr; } else { if (!m_autoSaveTimer) { m_autoSaveTimer = new QTimer(this); connect(m_autoSaveTimer, &QTimer::timeout, this, &ComposerViewBase::autoSaveMessage); } m_autoSaveTimer->start(m_autoSaveInterval); } } void ComposerViewBase::cleanupAutoSave() { delete m_autoSaveTimer; m_autoSaveTimer = nullptr; if (!mailIdIsEmpty()) { qCDebug(EDITOR_LOG) << "deleting autosave files" << mailId(); // Delete the autosave files QDir autoSaveDir(DraftManager::autosaveDirectory()); // Filter out only this composer window's autosave files const QStringList autoSaveFilter{mailId() + QLatin1String("*")}; autoSaveDir.setNameFilters(autoSaveFilter); // Return the files to be removed const QStringList autoSaveFiles = autoSaveDir.entryList(); qCDebug(EDITOR_LOG) << "There are" << autoSaveFiles.count() << "to be deleted."; // Delete each file for (const QString &file : autoSaveFiles) { autoSaveDir.remove(file); } m_autoSaveUUID.clear(); } } void ComposerViewBase::generateMessage(std::function)> callback) { auto composer = new Composer(); fillComposer(composer); composer->setAutoSave(true); m_composers.append(composer); connect(composer, &MessageComposer::Composer::result, this, [composer, callback]() { callback(composer->resultMessages()); }); composer->start(); } void ComposerViewBase::autoSaveMessage() { qCDebug(EDITOR_LOG) << "Autosaving message"; if (m_autoSaveTimer) { m_autoSaveTimer->stop(); } if (!m_composers.isEmpty()) { // This may happen if e.g. the autosave timer calls applyChanges. qCDebug(EDITOR_LOG) << "Autosave: Called while composer active; ignoring. Number of composer " << m_composers.count(); return; } auto composer = new Composer(); fillComposer(composer); composer->setAutoSave(true); m_composers.append(composer); connect(composer, &MessageComposer::Composer::result, this, &ComposerViewBase::slotAutoSaveComposeResult); composer->start(); } - void ComposerViewBase::slotSaveDraft() +void ComposerViewBase::slotSaveDraft() { qCDebug(EDITOR_LOG) << "Saving draft"; m_composers.clear(); auto composer = new Composer(); fillComposer(composer); composer->setDraft(true); composer->setSignAndEncrypt(false, true); const auto normalized = GpgME::UserID::addrSpecFromString(m_identity.fullEmailAddr().toUtf8().constData()); const auto normalizedSender = QString::fromUtf8(normalized.c_str()); auto encryptionKeys = Kleo::KeyCache::instance()->findByEMailAddress(normalizedSender.toStdString()); std::vector filteredEncryptionKeys; // ensure all keys are valid for (const auto &key : encryptionKeys) { if (strlen(key.userID(0).email()) > 0) { filteredEncryptionKeys.push_back(key); } } Q_ASSERT(!filteredEncryptionKeys.empty()); - composer->setEncryptionKeys({ - QPair>{{normalizedSender}, filteredEncryptionKeys} - }); + composer->setEncryptionKeys({QPair>{{normalizedSender}, filteredEncryptionKeys}}); composer->setCryptoMessageFormat(m_cryptoMessageFormat); m_composers.append(composer); connect(composer, &MessageComposer::Composer::result, this, &ComposerViewBase::slotSaveDraftComposeResult); composer->start(); } void ComposerViewBase::setAutoSaveFileName(const QString &fileName) { m_autoSaveUUID = fileName; Q_EMIT modified(true); } void ComposerViewBase::slotAutoSaveComposeResult(KJob *job) { using MessageComposer::Composer; Q_ASSERT(dynamic_cast(job)); auto composer = static_cast(job); if (composer->error() == Composer::NoError) { Q_ASSERT(m_composers.contains(composer)); // The messages were composed successfully. Only save the first message, there should // only be one anyway, since crypto is disabled. qCDebug(EDITOR_LOG) << "NoError."; writeAutoSaveToDisk(composer->resultMessages().constFirst()); Q_ASSERT(composer->resultMessages().size() == 1); if (m_autoSaveInterval > 0) { updateAutoSave(); } } else if (composer->error() == MessageComposer::Composer::UserCancelledError) { // The job warned the user about something, and the user chose to return // to the message. Nothing to do. qCDebug(EDITOR_LOG) << "UserCancelledError."; Q_EMIT failed(i18n("Job cancelled by the user"), AutoSave); } else { qCDebug(EDITOR_LOG) << "other Error."; Q_EMIT failed(i18n("Could not autosave message: %1", job->errorString()), AutoSave); } m_composers.removeAll(composer); } void ComposerViewBase::slotSaveDraftComposeResult(KJob *job) { using MessageComposer::Composer; Q_ASSERT(dynamic_cast(job)); auto composer = static_cast(job); if (composer->error() == Composer::NoError) { Q_ASSERT(m_composers.contains(composer)); // The messages were composed successfully. Only save the first message, there should // only be one anyway, since crypto is disabled. qCDebug(EDITOR_LOG) << "NoError."; writeDraftToDisk(composer->resultMessages().constFirst()); Q_ASSERT(composer->resultMessages().size() == 1); Q_EMIT sentSuccessfully(); } else if (composer->error() == MessageComposer::Composer::UserCancelledError) { // The job warned the user about something, and the user chose to return // to the message. Nothing to do. qCDebug(EDITOR_LOG) << "UserCancelledError."; Q_EMIT failed(i18n("Job cancelled by the user"), Draft); } else { qCDebug(EDITOR_LOG) << "other Error." << composer->error(); Q_EMIT failed(i18n("Could not save message as draft: %1", job->errorString()), Draft); } m_composers.removeAll(composer); } void ComposerViewBase::writeDraftToDisk(const KMime::Message::Ptr &message) { QDir().mkpath(DraftManager::draftDirectory()); const QString filename = DraftManager::draftDirectory() + mailId(); QSaveFile file(filename); QString errorMessage; qCDebug(EDITOR_LOG) << "Writing message to disk as" << filename; if (file.open(QIODevice::WriteOnly)) { file.setPermissions(QFile::ReadUser | QFile::WriteUser); if (file.write(message->encodedContent()) != static_cast(message->encodedContent().size())) { errorMessage = i18n("Could not write all data to file."); } else { if (!file.commit()) { errorMessage = i18n("Could not finalize the file."); } } } else { errorMessage = i18n("Could not open file."); } if (!errorMessage.isEmpty()) { qCWarning(EDITOR_LOG) << "Saving draft failed:" << errorMessage << file.errorString() << "mailId" << mailId(); if (!m_autoSaveErrorShown) { KMessageBox::error(m_parentWidget, i18n("Saving the draft message as %1 failed.\n" "%2\n" "Reason: %3", filename, errorMessage, file.errorString()), i18nc("@title:window", "Autosaving Message Failed")); // Error dialog shown, hide the errors the next time m_autoSaveErrorShown = true; } } else { // No error occurred, the next error should be shown again m_autoSaveErrorShown = false; } file.commit(); message->clear(); } void ComposerViewBase::writeAutoSaveToDisk(const KMime::Message::Ptr &message) { QDir().mkpath(DraftManager::autosaveDirectory()); const QString filename = DraftManager::autosaveDirectory() + mailId(); QSaveFile file(filename); QString errorMessage; qCDebug(EDITOR_LOG) << "Writing message to disk as" << filename; if (file.open(QIODevice::WriteOnly)) { file.setPermissions(QFile::ReadUser | QFile::WriteUser); if (file.write(message->encodedContent()) != static_cast(message->encodedContent().size())) { errorMessage = i18n("Could not write all data to file."); } else { if (!file.commit()) { errorMessage = i18n("Could not finalize the file."); } } } else { errorMessage = i18n("Could not open file."); } if (!errorMessage.isEmpty()) { qCWarning(EDITOR_LOG) << "Auto saving failed:" << errorMessage << file.errorString() << "mailId=" << mailId(); if (!m_autoSaveErrorShown) { KMessageBox::error(m_parentWidget, i18n("Autosaving the message as %1 failed.\n" "%2\n" "Reason: %3", filename, errorMessage, file.errorString()), i18nc("@title:window", "Autosaving Message Failed")); // Error dialog shown, hide the errors the next time m_autoSaveErrorShown = true; } } else { // No error occurred, the next error should be shown again m_autoSaveErrorShown = false; } file.commit(); message->clear(); } void ComposerViewBase::addAttachment(const QUrl &url, const QString &comment, bool sync) { Q_UNUSED(comment) qCDebug(EDITOR_LOG) << "adding attachment with url:" << url; if (sync) { m_attachmentController->addAttachmentUrlSync(url); } else { m_attachmentController->addAttachment(url); } } void ComposerViewBase::addAttachment(const QString &name, const QString &filename, const QString &charset, const QByteArray &data, const QByteArray &mimeType) { MessageCore::AttachmentPart::Ptr attachment = MessageCore::AttachmentPart::Ptr(new MessageCore::AttachmentPart()); if (!data.isEmpty()) { attachment->setName(name); attachment->setFileName(filename); attachment->setData(data); attachment->setCharset(charset.toLatin1()); attachment->setMimeType(mimeType); // TODO what about the other fields? m_attachmentController->addAttachment(attachment); } } void ComposerViewBase::addAttachmentPart(KMime::Content *partToAttach) { MessageCore::AttachmentPart::Ptr part(new MessageCore::AttachmentPart); if (partToAttach->contentType()->mimeType() == "multipart/digest" || partToAttach->contentType(false)->mimeType() == "message/rfc822") { // if it is a digest or a full message, use the encodedContent() of the attachment, // which already has the proper headers part->setData(partToAttach->encodedContent()); } else { part->setData(partToAttach->decodedContent()); } part->setMimeType(partToAttach->contentType(false)->mimeType()); if (auto cd = partToAttach->contentDescription(false)) { part->setDescription(cd->asUnicodeString()); } if (auto ct = partToAttach->contentType(false)) { if (ct->hasParameter(QStringLiteral("name"))) { part->setName(ct->parameter(QStringLiteral("name"))); } } if (auto cd = partToAttach->contentDisposition(false)) { part->setFileName(cd->filename()); part->setInline(cd->disposition() == KMime::Headers::CDinline); } if (part->name().isEmpty() && !part->fileName().isEmpty()) { part->setName(part->fileName()); } if (part->fileName().isEmpty() && !part->name().isEmpty()) { part->setFileName(part->name()); } m_attachmentController->addAttachment(part); } void ComposerViewBase::fillComposer(MessageComposer::Composer *composer) { fillComposer(composer, UseUnExpandedRecipients, false); } void ComposerViewBase::fillComposer(MessageComposer::Composer *composer, ComposerViewBase::RecipientExpansion expansion, bool autoresize) { fillInfoPart(composer->infoPart(), expansion); fillGlobalPart(composer->globalPart()); m_editor->fillComposerTextPart(composer->textPart()); fillInfoPart(composer->infoPart(), expansion); if (m_attachmentModel) { composer->addAttachmentParts(m_attachmentModel->attachments(), autoresize); } else { - qDebug() << "fillComposer" << "no model"; + qDebug() << "fillComposer" + << "no model"; } } //----------------------------------------------------------------------------- QString ComposerViewBase::to() const { if (m_recipientsEditor) { return MessageComposer::Util::cleanedUpHeaderString(m_recipientsEditor->recipientString(Recipient::To)); } return {}; } //----------------------------------------------------------------------------- QString ComposerViewBase::cc() const { if (m_recipientsEditor) { return MessageComposer::Util::cleanedUpHeaderString(m_recipientsEditor->recipientString(Recipient::Cc)); } return {}; } //----------------------------------------------------------------------------- QString ComposerViewBase::bcc() const { if (m_recipientsEditor) { return MessageComposer::Util::cleanedUpHeaderString(m_recipientsEditor->recipientString(Recipient::Bcc)); } return {}; } QString ComposerViewBase::from() const { return MessageComposer::Util::cleanedUpHeaderString(m_from); } QString ComposerViewBase::replyTo() const { if (m_recipientsEditor) { return MessageComposer::Util::cleanedUpHeaderString(m_recipientsEditor->recipientString(Recipient::ReplyTo)); } return {}; } QString ComposerViewBase::subject() const { return MessageComposer::Util::cleanedUpHeaderString(m_subject); } void ComposerViewBase::setIdentity(const KIdentityManagementCore::Identity &identity) { m_identity = identity; } KIdentityManagementCore::Identity ComposerViewBase::identity() const { return m_identity; } void ComposerViewBase::setParentWidgetForGui(QWidget *w) { m_parentWidget = w; } void ComposerViewBase::setAttachmentController(MessageComposer::AttachmentControllerBase *controller) { m_attachmentController = controller; } MessageComposer::AttachmentControllerBase *ComposerViewBase::attachmentController() { return m_attachmentController; } void ComposerViewBase::setAttachmentModel(MessageComposer::AttachmentModel *model) { m_attachmentModel = model; } MessageComposer::AttachmentModel *ComposerViewBase::attachmentModel() { return m_attachmentModel; } void ComposerViewBase::setRecipientsEditor(RecipientsEditor *recEditor) { m_recipientsEditor = recEditor; } RecipientsEditor *ComposerViewBase::recipientsEditor() { return m_recipientsEditor; } void ComposerViewBase::setSignatureController(MessageComposer::SignatureController *sigController) { m_signatureController = sigController; } MessageComposer::SignatureController *ComposerViewBase::signatureController() { return m_signatureController; } -void ComposerViewBase::updateRecipients(const KIdentityManagementCore::Identity &ident, - const KIdentityManagementCore::Identity &oldIdent, - Recipient::Type type) +void ComposerViewBase::updateRecipients(const KIdentityManagementCore::Identity &ident, const KIdentityManagementCore::Identity &oldIdent, Recipient::Type type) { QString oldIdentList; QString newIdentList; if (type == Recipient::Bcc) { oldIdentList = oldIdent.bcc(); newIdentList = ident.bcc(); } else if (type == Recipient::Cc) { oldIdentList = oldIdent.cc(); newIdentList = ident.cc(); } else if (type == Recipient::ReplyTo) { oldIdentList = oldIdent.replyToAddr(); newIdentList = ident.replyToAddr(); } else { return; } if (oldIdentList != newIdentList) { const auto oldRecipients = KMime::Types::Mailbox::listFromUnicodeString(oldIdentList); for (const KMime::Types::Mailbox &recipient : oldRecipients) { m_recipientsEditor->removeRecipient(recipient.prettyAddress(), type); } const auto newRecipients = KMime::Types::Mailbox::listFromUnicodeString(newIdentList); for (const KMime::Types::Mailbox &recipient : newRecipients) { m_recipientsEditor->addRecipient(recipient.prettyAddress(), type); } m_recipientsEditor->setFocusBottom(); } } void ComposerViewBase::identityChanged(const KIdentityManagementCore::Identity &ident, const KIdentityManagementCore::Identity &oldIdent, bool msgCleared) { updateRecipients(ident, oldIdent, Recipient::Bcc); updateRecipients(ident, oldIdent, Recipient::Cc); updateRecipients(ident, oldIdent, Recipient::ReplyTo); KIdentityManagementCore::Signature oldSig = const_cast(oldIdent).signature(); KIdentityManagementCore::Signature newSig = const_cast(ident).signature(); // replace existing signatures const bool replaced = editor()->composerSignature()->replaceSignature(oldSig, newSig); // Just append the signature if there was no old signature if (!replaced && (msgCleared || oldSig.rawText().isEmpty())) { signatureController()->applySignature(newSig); } m_editor->setAutocorrectionLanguage(ident.autocorrectionLanguage()); } void ComposerViewBase::setEditor(MessageComposer::RichTextComposerNg *editor) { m_editor = editor; m_editor->document()->setModified(false); } MessageComposer::RichTextComposerNg *ComposerViewBase::editor() const { return m_editor; } void ComposerViewBase::setFrom(const QString &from) { m_from = from; } void ComposerViewBase::setSubject(const QString &subject) { m_subject = subject; } void ComposerViewBase::setAutoSaveInterval(int interval) { m_autoSaveInterval = interval; } void ComposerViewBase::setCryptoOptions(bool sign, bool encrypt, Kleo::CryptoMessageFormat format, bool neverEncryptDrafts) { m_sign = sign; m_encrypt = encrypt; m_cryptoMessageFormat = format; m_neverEncrypt = neverEncryptDrafts; } void ComposerViewBase::setCharsets(const QList &charsets) { m_charsets = charsets; } void ComposerViewBase::setMDNRequested(bool mdnRequested) { m_mdnRequested = mdnRequested; } void ComposerViewBase::setUrgent(bool urgent) { m_urgent = urgent; } int ComposerViewBase::autoSaveInterval() const { return m_autoSaveInterval; } //----------------------------------------------------------------------------- void ComposerViewBase::collectImages(KMime::Content *root) { if (KMime::Content *n = Util::findTypeInMessage(root, "multipart", "alternative")) { KMime::Content *parentnode = n->parent(); if (parentnode && parentnode->contentType()->isMultipart() && parentnode->contentType()->subType() == "related") { KMime::Content *node = MessageCore::NodeHelper::nextSibling(n); while (node) { if (node->contentType()->isImage()) { qCDebug(EDITOR_LOG) << "found image in multipart/related : " << node->contentType()->name(); QImage img; img.loadFromData(node->decodedContent()); m_editor->composerControler()->composerImages()->loadImage( img, QString::fromLatin1(QByteArray(QByteArrayLiteral("cid:") + node->contentID()->identifier())), node->contentType()->name()); } node = MessageCore::NodeHelper::nextSibling(node); } } } } //----------------------------------------------------------------------------- bool ComposerViewBase::inlineSigningEncryptionSelected() const { if (!m_sign && !m_encrypt) { return false; } return m_cryptoMessageFormat == Kleo::InlineOpenPGPFormat; } bool ComposerViewBase::hasMissingAttachments(const QStringList &attachmentKeywords) { if (attachmentKeywords.isEmpty()) { return false; } if (m_attachmentModel && m_attachmentModel->rowCount() > 0) { return false; } return MessageComposer::Util::hasMissingAttachments(attachmentKeywords, m_editor->document(), subject()); } ComposerViewBase::MissingAttachment ComposerViewBase::checkForMissingAttachments(const QStringList &attachmentKeywords) { if (!hasMissingAttachments(attachmentKeywords)) { return NoMissingAttachmentFound; } const int rc = KMessageBox::warningTwoActionsCancel(m_editor, i18n("The message you have composed seems to refer to an " "attached file but you have not attached anything.\n" "Do you want to attach a file to your message?"), i18nc("@title:window", "File Attachment Reminder"), KGuiItem(i18n("&Attach File..."), QLatin1String("mail-attachment")), KGuiItem(i18n("&Send as Is"), QLatin1String("mail-send"))); if (rc == KMessageBox::Cancel) { return FoundMissingAttachmentAndCancel; } if (rc == KMessageBox::ButtonCode::PrimaryAction) { m_attachmentController->showAddAttachmentFileDialog(); return FoundMissingAttachmentAndAddedAttachment; } return FoundMissingAttachmentAndSending; } void ComposerViewBase::markAllAttachmentsForSigning(bool sign) { if (m_attachmentModel) { const auto attachments = m_attachmentModel->attachments(); for (MessageCore::AttachmentPart::Ptr attachment : attachments) { attachment->setSigned(sign); } } } void ComposerViewBase::markAllAttachmentsForEncryption(bool encrypt) { if (m_attachmentModel) { const auto attachments = m_attachmentModel->attachments(); for (MessageCore::AttachmentPart::Ptr attachment : attachments) { attachment->setEncrypted(encrypt); } } } bool ComposerViewBase::requestDeleveryConfirmation() const { return m_requestDeleveryConfirmation; } void ComposerViewBase::setRequestDeleveryConfirmation(bool requestDeleveryConfirmation) { m_requestDeleveryConfirmation = requestDeleveryConfirmation; } KMime::Message::Ptr ComposerViewBase::msg() const { return m_msg; } std::shared_ptr ComposerViewBase::expiryChecker() { if (!mExpiryChecker) { mExpiryChecker.reset(new Kleo::ExpiryChecker{Kleo::ExpiryCheckerSettings{encryptOwnKeyNearExpiryWarningThresholdInDays(), encryptKeyNearExpiryWarningThresholdInDays(), encryptRootCertNearExpiryWarningThresholdInDays(), encryptChainCertNearExpiryWarningThresholdInDays()}}); } return mExpiryChecker; } QString ComposerViewBase::mailId() const { if (m_autoSaveUUID.isEmpty()) { m_autoSaveUUID = QUuid::createUuid().toString(QUuid::WithoutBraces); } return m_autoSaveUUID; } bool ComposerViewBase::mailIdIsEmpty() const { return m_autoSaveUUID.isEmpty(); } #include "moc_composerviewbase.cpp" diff --git a/client/editor/composerviewbase.h b/client/editor/composerviewbase.h index 4d04fa4..d22374b 100644 --- a/client/editor/composerviewbase.h +++ b/client/editor/composerviewbase.h @@ -1,370 +1,369 @@ /* SPDX-FileCopyrightText: 2010 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.com SPDX-FileCopyrightText: 2010 Leo Franchi SPDX-License-Identifier: LGPL-2.0-or-later */ #pragma once #include "recipient.h" #include +#include "identity/identity.h" #include #include #include #include -#include "identity/identity.h" class QTimer; class KJob; class QWidget; class ComposerViewBaseTest; namespace Sonnet { class DictionaryComboBox; } namespace Kleo { class ExpiryChecker; class KeyResolver; } class IdentityCombo; class IdentityManager; class RecipientsEditor; namespace MessageComposer { class RichTextComposerNg; class InfoPart; class GlobalPart; class Composer; class AttachmentControllerBase; class AttachmentModel; class SignatureController; /** * @brief The ComposerViewBase class */ class ComposerViewBase : public QObject { Q_OBJECT public: explicit ComposerViewBase(QObject *parent = nullptr, QWidget *widget = nullptr); ~ComposerViewBase() override; enum Confirmation { LetUserConfirm, NoConfirmationNeeded, }; enum MissingAttachment { NoMissingAttachmentFound, FoundMissingAttachmentAndSending, FoundMissingAttachmentAndAddedAttachment, FoundMissingAttachmentAndCancel, }; enum FailedType { Sending, AutoSave, Draft, }; void setBearerToken(const QByteArray &bearerToken); /** * Set the message to be opened in the composer window, and set the internal data * structures to keep track of it. */ void setMessage(const KMime::Message::Ptr &newMsg); /** * Send the message with the specified method, saving it in the specified folder. */ void send(); /** * Returns true if there is at least one composer job running. */ [[nodiscard]] bool isComposing() const; /** * Add the given attachment to the message. */ void addAttachment(const QUrl &url, const QString &comment, bool sync); void addAttachment(const QString &name, const QString &filename, const QString &charset, const QByteArray &data, const QByteArray &mimeType); void addAttachmentPart(KMime::Content *part); void fillComposer(MessageComposer::Composer *composer); /** * Header fields in recipients editor. */ [[nodiscard]] QString to() const; [[nodiscard]] QString cc() const; [[nodiscard]] QString bcc() const; [[nodiscard]] QString from() const; [[nodiscard]] QString replyTo() const; [[nodiscard]] QString subject() const; /// \returns the mail id /// /// The mail id allows to uniquely identify a message composer dialog. It's used /// when autosaving a draft and as identifier when sending an email. [[nodiscard]] QString mailId() const; /// \returns whether the mail id is empty [[nodiscard]] bool mailIdIsEmpty() const; /// \returns the current identitfy [[nodiscard]] KIdentityManagementCore::Identity identity() const; /// Set the current identity. void setIdentity(const KIdentityManagementCore::Identity &identiy); /// The following are for setting the various options and widgets in the composer. void setAttachmentModel(MessageComposer::AttachmentModel *model); [[nodiscard]] MessageComposer::AttachmentModel *attachmentModel(); void setAttachmentController(MessageComposer::AttachmentControllerBase *controller); [[nodiscard]] MessageComposer::AttachmentControllerBase *attachmentController(); void setRecipientsEditor(RecipientsEditor *recEditor); [[nodiscard]] RecipientsEditor *recipientsEditor(); void setSignatureController(MessageComposer::SignatureController *sigController); [[nodiscard]] MessageComposer::SignatureController *signatureController(); void setEditor(MessageComposer::RichTextComposerNg *editor); [[nodiscard]] MessageComposer::RichTextComposerNg *editor() const; [[nodiscard]] Sonnet::DictionaryComboBox *dictionary() const; void setDictionary(Sonnet::DictionaryComboBox *dictionary); /** * Widgets for editing differ in client classes, so * values are set before sending. */ void setFrom(const QString &from); void setSubject(const QString &subject); /** * The following are various settings the user can modify when composing a message. If they are not set, * the default values will be used. */ void setCryptoOptions(bool sign, bool encrypt, Kleo::CryptoMessageFormat format, bool neverEncryptDrafts = false); void setCharsets(const QList &charsets); void setMDNRequested(bool mdnRequested); void setUrgent(bool urgent); void setAutoSaveInterval(int interval); void setCustomHeader(const QMap &customHeader); /** * Enables/disables autosaving depending on the value of the autosave * interval. */ void updateAutoSave(); /** * Sets the filename to use when autosaving something. This is used when the client recovers * the autosave files: It calls this method, so that the composer uses the same filename again. * That way, the recovered autosave file is properly cleaned up in cleanupAutoSave(): */ void setAutoSaveFileName(const QString &fileName); /** * Stop autosaving and delete the autosaved message. */ void cleanupAutoSave(); void setParentWidgetForGui(QWidget *); /** * Check if the mail has references to attachments, but no attachments are added to it. * If missing attachments are found, a dialog to add new attachments is shown. * @param attachmentKeywords a list with the keywords that indicate an attachment should be present * @return NoMissingAttachmentFound, if there is attachment in email * FoundMissingAttachmentAndCancelSending, if mail might miss attachment but sending * FoundMissingAttachmentAndAddedAttachment, if mail might miss attachment and we added an attachment * FoundMissingAttachmentAndCancel, if mail might miss attachment and cancel sending */ [[nodiscard]] ComposerViewBase::MissingAttachment checkForMissingAttachments(const QStringList &attachmentKeywords); [[nodiscard]] bool hasMissingAttachments(const QStringList &attachmentKeywords); void saveMailSettings(); [[nodiscard]] QDate followUpDate() const; void setFollowUpDate(const QDate &followUpDate); void clearFollowUp(); [[nodiscard]] KMime::Message::Ptr msg() const; [[nodiscard]] bool requestDeleveryConfirmation() const; void setRequestDeleveryConfirmation(bool requestDeleveryConfirmation); [[nodiscard]] std::shared_ptr expiryChecker(); public Q_SLOTS: void identityChanged(const KIdentityManagementCore::Identity &ident, const KIdentityManagementCore::Identity &oldIdent, bool msgCleared = false); /// Save the message in the autosave folder. /// /// This message will be reopened automatically in the next section if /// it wasn't cleaned up with cleanupAutoSave void autoSaveMessage(); /// Save the message as a draft. void slotSaveDraft(); /// Generate a KMime::Message::Ptr from the current state of the composer /// /// \param callback The callback will be called when the message is generated. void generateMessage(std::function)> callback); Q_SIGNALS: /** * Message sending completed successfully. */ void sentSuccessfully(); /** * Message sending failed with given error message. */ void failed(const QString &errorMessage, MessageComposer::ComposerViewBase::FailedType type = Sending); /** * The composer was modified. This can happen behind the users' back * when, for example, and autosaved message was recovered. */ void modified(bool isModified); /** * Enabling or disabling HTML in the editor is affected * by various client options, so when that would otherwise happen, * hand it off to the client to enact it for real. */ void disableHtml(MessageComposer::ComposerViewBase::Confirmation); void enableHtml(); void tooManyRecipient(bool); private Q_SLOTS: void slotSendComposeResult(KJob *); void slotAutoSaveComposeResult(KJob *job); void slotSaveDraftComposeResult(KJob *job); void slotComposerCreated(); Q_SIGNALS: void composerCreated(); private: /** * Searches the mime tree, where root is the root node, for embedded images, * extracts them froom the body and adds them to the editor. */ void collectImages(KMime::Content *root); [[nodiscard]] bool inlineSigningEncryptionSelected() const; /** * Applies the user changes to the message object of the composer * and signs/encrypts the message if activated. * Disables the controls of the composer window. */ void readyForSending(); enum RecipientExpansion { UseExpandedRecipients, UseUnExpandedRecipients, }; void fillComposer(MessageComposer::Composer *composer, ComposerViewBase::RecipientExpansion expansion, bool autoresize); /// Fill the composer with the signing/encryption key of the sender as well as the email /// addresses from the recipients. [[nodiscard]] Kleo::KeyResolver *fillKeyResolver(bool encryptSomething); /// Create one or multiple MessageComposer::Composer depending on the crypto settings /// Emits composerCreated when finished. void generateCryptoMessages(); void fillGlobalPart(MessageComposer::GlobalPart *globalPart); void fillInfoPart(MessageComposer::InfoPart *part, RecipientExpansion expansion); void queueMessage(const KMime::Message::Ptr &message); - void - updateRecipients(const KIdentityManagementCore::Identity &ident, const KIdentityManagementCore::Identity &oldIdent, Recipient::Type type); + void updateRecipients(const KIdentityManagementCore::Identity &ident, const KIdentityManagementCore::Identity &oldIdent, Recipient::Type type); void markAllAttachmentsForSigning(bool sign); void markAllAttachmentsForEncryption(bool encrypt); /** * Writes out autosave data to the disk from the KMime::Message message. * Also appends the msgNum to the filename as a message can have a number of * KMime::Messages */ void writeAutoSaveToDisk(const KMime::Message::Ptr &message); /** * Writes out draft data to the disk from the KMime::Message message. * Also appends the msgNum to the filename as a message can have a number of * KMime::Messages */ void writeDraftToDisk(const KMime::Message::Ptr &message); /** * Returns the autosave interval in milliseconds (as needed for QTimer). */ int autoSaveInterval() const; /** * Initialize autosaving (timer and filename). */ void initAutoSave(); KMime::Message::Ptr m_msg; MessageComposer::AttachmentControllerBase *m_attachmentController = nullptr; MessageComposer::AttachmentModel *m_attachmentModel = nullptr; MessageComposer::SignatureController *m_signatureController = nullptr; RecipientsEditor *m_recipientsEditor = nullptr; IdentityCombo *m_identityCombo = nullptr; KIdentityManagementCore::Identity m_identity; MessageComposer::RichTextComposerNg *m_editor = nullptr; Sonnet::DictionaryComboBox *m_dictionary = nullptr; QWidget *m_parentWidget = nullptr; // List of active composer jobs. For example, saving as draft, autosaving and printing // all create a composer, which is added to this list as long as it is active. // Used mainly to prevent closing the window if a composer is active QList m_composers; bool m_sign = false; bool m_encrypt = false; bool m_neverEncrypt = false; bool m_mdnRequested = false; bool m_urgent = false; bool m_requestDeleveryConfirmation = false; Kleo::CryptoMessageFormat m_cryptoMessageFormat; QString mExpandedFrom; QString m_from; QString m_subject; QStringList mExpandedTo, mExpandedCc, mExpandedBcc, mExpandedReplyTo; QList m_charsets; QMap m_customHeader; int m_pendingQueueJobs = 0; QTimer *m_autoSaveTimer = nullptr; mutable QString m_autoSaveUUID; bool m_autoSaveErrorShown = false; // Stops an error message being shown every time autosave is executed. int m_autoSaveInterval; std::shared_ptr mExpiryChecker; QDate mFollowUpDate; QByteArray m_bearerToken; friend ComposerViewBaseTest; }; } // namespace diff --git a/client/editor/composerwindow.cpp b/client/editor/composerwindow.cpp index 4b58f17..37606ac 100644 --- a/client/editor/composerwindow.cpp +++ b/client/editor/composerwindow.cpp @@ -1,1762 +1,1756 @@ // SPDX-FileCopyrightText: 2023 g10 code Gmbh // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "composerwindow.h" // Qt includes -#include #include -#include -#include #include -#include -#include -#include -#include +#include +#include #include -#include -#include -#include #include +#include +#include +#include #include #include #include #include -#include +#include +#include +#include +#include +#include +#include // KDE includes -#include -#include +#include "identity/identity.h" +#include +#include #include +#include +#include #include -#include +#include +#include #include -#include #include +#include #include +#include #include -#include -#include -#include #include -#include -#include #include #include -#include -#include "identity/identity.h" +#include +#include -#include -#include #include #include +#include +#include #include #include // Gpgme includes #include #include // App includes -#include "../identity/identitymanager.h" #include "../identity/identitydialog.h" -#include "recipientseditor.h" -#include "nearexpirywarning.h" -#include "composerviewbase.h" -#include "richtextcomposerng.h" -#include "signaturecontroller.h" -#include "job/saveasfilejob.h" -#include "job/inserttextfilejob.h" +#include "../identity/identitymanager.h" #include "attachment/attachmentcontroller.h" -#include "attachment/attachmentview.h" #include "attachment/attachmentmodel.h" +#include "attachment/attachmentview.h" +#include "composerviewbase.h" +#include "draft/draftmanager.h" +#include "job/inserttextfilejob.h" +#include "job/saveasfilejob.h" #include "kmcomposerglobalaction.h" #include "mailtemplates.h" #include "messagecomposersettings.h" +#include "nearexpirywarning.h" +#include "recipientseditor.h" +#include "richtextcomposerng.h" +#include "signaturecontroller.h" #include "spellcheckerconfigdialog.h" #include "websocketclient.h" -#include "draft/draftmanager.h" using namespace Qt::Literals::StringLiterals; using namespace std::chrono_literals; -namespace { +namespace +{ inline bool containsSMIME(unsigned int f) { return f & (Kleo::SMIMEFormat | Kleo::SMIMEOpaqueFormat); } inline bool containsOpenPGP(unsigned int f) { return f & (Kleo::OpenPGPMIMEFormat | Kleo::InlineOpenPGPFormat); } auto findSendersUid(const std::string &addrSpec, const std::vector &userIds) { return std::find_if(userIds.cbegin(), userIds.cend(), [&addrSpec](const auto &uid) { return uid.addrSpec() == addrSpec || (uid.addrSpec().empty() && std::string(uid.email()) == addrSpec) || (uid.addrSpec().empty() && (!uid.email() || !*uid.email()) && uid.name() == addrSpec); }); } } ComposerWindow::ComposerWindow(const QString &from, const QString &name, const QByteArray &bearerToken, QWidget *parent) : KXmlGuiWindow(parent) , mFrom(from) , mMainWidget(new QWidget(this)) , mComposerBase(new MessageComposer::ComposerViewBase(this)) , mHeadersToEditorSplitter(new QSplitter(Qt::Vertical, mMainWidget)) , mHeadersArea(new QWidget(mHeadersToEditorSplitter)) , mGrid(new QGridLayout(mHeadersArea)) , mLblFrom(new QLabel(i18nc("sender address field", "From:"), mHeadersArea)) , mButtonFrom(new QPushButton(mHeadersArea)) , mRecipientEditor(new RecipientsEditor(mHeadersArea)) , mLblSubject(new QLabel(i18nc("@label:textbox Subject of email.", "Subject:"), mHeadersArea)) , mEdtSubject(new QLineEdit(mHeadersArea)) , mRichTextComposer(new MessageComposer::RichTextComposerNg(this)) , mRichTextEditorWidget(new TextCustomEditor::RichTextEditorWidget(mRichTextComposer, mMainWidget)) , mNearExpiryWarning(new NearExpiryWarning(this)) , mGlobalAction(new KMComposerGlobalAction(this, this)) , mKeyCache(Kleo::KeyCache::mutableInstance()) { connect(mKeyCache.get(), &Kleo::KeyCache::keyListingDone, this, [this](const GpgME::KeyListResult &result) { Q_UNUSED(result); mRunKeyResolverTimer->start(); }); bool isNew = false; mIdentity = IdentityManager::self().fromEmail(from, isNew); mEdtFrom = new QLabel(mHeadersArea); if (isNew) { // fill the idenity with default fields auto dlg = new KMail::IdentityDialog; mIdentity.setFullName(name); mIdentity.setWarnNotEncrypt(true); dlg->setIdentity(mIdentity); connect(dlg, &KMail::IdentityDialog::keyListingFinished, this, [this, dlg]() { dlg->updateIdentity(mIdentity); IdentityManager::self().updateIdentity(mIdentity); }); } mComposerBase->setBearerToken(bearerToken); mMainWidget->resize(800, 600); setCentralWidget(mMainWidget); setWindowTitle(i18nc("@title:window", "Composer")); setMinimumSize(200, 200); mHeadersToEditorSplitter->setObjectName(QStringLiteral("mHeadersToEditorSplitter")); mHeadersToEditorSplitter->setChildrenCollapsible(false); auto v = new QVBoxLayout(mMainWidget); v->setContentsMargins({}); v->addWidget(mNearExpiryWarning); v->addWidget(mHeadersToEditorSplitter); mHeadersArea->setSizePolicy(mHeadersToEditorSplitter->sizePolicy().horizontalPolicy(), QSizePolicy::Expanding); mHeadersToEditorSplitter->addWidget(mHeadersArea); const QList defaultSizes{0}; mHeadersToEditorSplitter->setSizes(defaultSizes); mGrid->setColumnStretch(0, 1); mGrid->setColumnStretch(1, 100); mGrid->setRowStretch(3 + 1, 100); int row = 0; mRunKeyResolverTimer = new QTimer(this); mRunKeyResolverTimer->setSingleShot(true); mRunKeyResolverTimer->setInterval(500ms); connect(mRunKeyResolverTimer, &QTimer::timeout, this, &ComposerWindow::runKeyResolver); // From mLblFrom->setObjectName(QStringLiteral("fromLineLabel")); mLblFrom->setFixedWidth(mRecipientEditor->setFirstColumnWidth(0)); mLblFrom->setBuddy(mEdtFrom); auto fromWrapper = new QWidget(mHeadersArea); auto fromWrapperLayout = new QHBoxLayout(fromWrapper); fromWrapperLayout->setContentsMargins({}); mEdtFrom->installEventFilter(this); mEdtFrom->setText(mFrom); mEdtFrom->setObjectName(QStringLiteral("fromLine")); fromWrapperLayout->addWidget(mEdtFrom); mComposerBase->setIdentity(mIdentity); mButtonFrom->setText(i18nc("@action:button", "Configure")); mButtonFrom->setIcon(QIcon::fromTheme(u"configure-symbolic"_s)); connect(mButtonFrom, &QPushButton::clicked, this, &ComposerWindow::slotEditIdentity); fromWrapperLayout->addWidget(mButtonFrom); mGrid->addWidget(mLblFrom, row, 0); mGrid->addWidget(fromWrapper, row, 1); row++; // Recipients mGrid->addWidget(mRecipientEditor, row, 0, 1, 2); mComposerBase->setRecipientsEditor(mRecipientEditor); mRecipientEditor->setCompletionMode(KCompletion::CompletionPopup); connect(mRecipientEditor, &RecipientsEditor::lineAdded, this, [this](KPIM::MultiplyingLine *line) { slotRecipientEditorLineAdded(qobject_cast(line)); }); row++; // Subject mEdtSubject->setObjectName(u"subjectLine"_s); mLblSubject->setObjectName(u"subjectLineLabel"_s); mLblSubject->setBuddy(mEdtSubject); mGrid->addWidget(mLblSubject, row, 0); mGrid->addWidget(mEdtSubject, row, 1); row++; auto editorWidget = new QWidget(); auto vLayout = new QVBoxLayout(editorWidget); vLayout->setContentsMargins({}); vLayout->setSpacing(0); mHeadersToEditorSplitter->addWidget(editorWidget); // Message widget auto connectionLossWidget = new KMessageWidget(this); connectionLossWidget->hide(); connectionLossWidget->setWordWrap(true); connectionLossWidget->setPosition(KMessageWidget::Position::Header); vLayout->addWidget(connectionLossWidget); auto &websocketClient = WebsocketClient::self(); connect(&websocketClient, &WebsocketClient::closed, this, [connectionLossWidget](const QString &errorMessage) { connectionLossWidget->setText(errorMessage); connectionLossWidget->show(); }); connect(&websocketClient, &WebsocketClient::connected, this, [connectionLossWidget]() { connectionLossWidget->hide(); }); connect(&websocketClient, &WebsocketClient::emailSentSuccessfully, this, [this](const QString &id) { if (id == mComposerBase->mailId()) { auto &draftManager = DraftManager::self(); draftManager.remove(draftManager.draftById(id.toUtf8())); hide(); } }); // Rich text editor mRichTextComposer->setProperty("_breeze_borders_sides", QVariant::fromValue(QFlags{Qt::TopEdge})); mRichTextComposer->setProperty("_breeze_force_frame", true); mComposerBase->setEditor(mRichTextComposer); vLayout->addWidget(mRichTextEditorWidget); auto attachmentModel = new MessageComposer::AttachmentModel(this); auto attachmentView = new AttachmentView(attachmentModel, mHeadersToEditorSplitter); attachmentView->hideIfEmpty(); connect(attachmentView, &AttachmentView::modified, this, &ComposerWindow::setModified); auto attachmentController = new AttachmentController(attachmentModel, attachmentView, this); mComposerBase->setAttachmentController(attachmentController); mComposerBase->setAttachmentModel(attachmentModel); auto signatureController = new MessageComposer::SignatureController(this); connect(signatureController, &MessageComposer::SignatureController::enableHtml, this, &ComposerWindow::enableHtml); signatureController->setIdentity(mIdentity); signatureController->setEditor(mComposerBase->editor()); mComposerBase->setSignatureController(signatureController); connect(signatureController, &MessageComposer::SignatureController::signatureAdded, mComposerBase->editor()->externalComposer(), &KPIMTextEdit::RichTextExternalComposer::startExternalEditor); setupStatusBar(attachmentView->widget()); setupActions(); setStandardToolBarMenuEnabled(true); toolBar(u"mainToolBar"_s)->show(); connect(expiryChecker().get(), &Kleo::ExpiryChecker::expiryMessage, this, [&](const GpgME::Key &key, QString msg, Kleo::ExpiryChecker::ExpiryInformation info, bool isNewMessage) { Q_UNUSED(isNewMessage); if (info == Kleo::ExpiryChecker::OwnKeyExpired || info == Kleo::ExpiryChecker::OwnKeyNearExpiry) { const auto plainMsg = msg.replace(QStringLiteral("

"), QStringLiteral(" ")) .replace(QStringLiteral("

"), QStringLiteral(" ")) .replace(QStringLiteral("

"), QStringLiteral(" ")); mNearExpiryWarning->addInfo(plainMsg); mNearExpiryWarning->setWarning(info == Kleo::ExpiryChecker::OwnKeyExpired); mNearExpiryWarning->animatedShow(); } const QList lstLines = mRecipientEditor->lines(); for (KPIM::MultiplyingLine *line : lstLines) { auto recipient = line->data().dynamicCast(); if (recipient->key().primaryFingerprint() == key.primaryFingerprint()) { auto recipientLine = qobject_cast(line); QString iconName = QStringLiteral("emblem-warning"); if (info == Kleo::ExpiryChecker::OtherKeyExpired) { mAcceptedSolution = false; iconName = QStringLiteral("emblem-error"); const auto showCryptoIndicator = true; const auto encrypt = mEncryptAction->isChecked(); const bool showAllIcons = showCryptoIndicator && encrypt; if (!showAllIcons) { recipientLine->setIcon(QIcon(), msg); return; } } recipientLine->setIcon(QIcon::fromTheme(iconName), msg); return; } } }); // TODO make it possible to show this auto dictionaryComboBox = new Sonnet::DictionaryComboBox(this); dictionaryComboBox->hide(); mComposerBase->setDictionary(dictionaryComboBox); slotIdentityChanged(); runKeyResolver(); connect(mComposerBase, &MessageComposer::ComposerViewBase::sentSuccessfully, this, &ComposerWindow::slotSendSuccessful); connect(mComposerBase, &MessageComposer::ComposerViewBase::failed, this, [this](const QString &errorMessage) { KMessageBox::error(this, errorMessage); }); } void ComposerWindow::reset(const QString &fromAddress, const QString &name, const QByteArray &bearerToken) { mFrom = fromAddress; bool isNew = false; mIdentity = IdentityManager::self().fromEmail(fromAddress, isNew); if (isNew) { // fill the idenity with default fields auto dlg = new KMail::IdentityDialog; mIdentity.setFullName(name); dlg->setIdentity(mIdentity); connect(dlg, &KMail::IdentityDialog::keyListingFinished, this, [this, dlg]() { dlg->updateIdentity(mIdentity); IdentityManager::self().updateIdentity(mIdentity); }); } mComposerBase->setBearerToken(bearerToken); mEdtSubject->setText(QString()); mRecipientEditor->clear(); mComposerBase->editor()->setText(QString{}); mComposerBase->attachmentController()->clear(); const QList lstLines = mRecipientEditor->lines(); for (KPIM::MultiplyingLine *line : lstLines) { slotRecipientEditorLineAdded(qobject_cast(line)); } } void ComposerWindow::setupActions() { // Save as draft auto action = new QAction(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save as Encrypted &Draft"), this); actionCollection()->addAction(QStringLiteral("save_in_drafts"), action); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL | Qt::Key_S)); connect(action, &QAction::triggered, this, &ComposerWindow::slotSaveDraft); // Save as file action = new QAction(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save as &File"), this); actionCollection()->addAction(QStringLiteral("save_as_file"), action); connect(action, &QAction::triggered, this, &ComposerWindow::slotSaveAsFile); // Insert file action = new QAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("&Insert Text File..."), this); actionCollection()->addAction(QStringLiteral("insert_file"), action); connect(action, &QAction::triggered, this, &ComposerWindow::slotInsertFile); // Spellchecking mAutoSpellCheckingAction = new KToggleAction(QIcon::fromTheme(QStringLiteral("tools-check-spelling")), i18n("&Automatic Spellchecking"), this); actionCollection()->addAction(QStringLiteral("options_auto_spellchecking"), mAutoSpellCheckingAction); const bool spellChecking = MessageComposer::MessageComposerSettings::self()->autoSpellChecking(); mAutoSpellCheckingAction->setChecked(spellChecking); slotAutoSpellCheckingToggled(spellChecking); connect(mAutoSpellCheckingAction, &KToggleAction::toggled, this, &ComposerWindow::slotAutoSpellCheckingToggled); connect(mComposerBase->editor(), &TextCustomEditor::RichTextEditor::checkSpellingChanged, this, &ComposerWindow::slotAutoSpellCheckingToggled); action = new QAction(QIcon::fromTheme(QStringLiteral("tools-check-spelling")), i18n("&Spellchecker..."), this); action->setIconText(i18n("Spellchecker")); actionCollection()->addAction(QStringLiteral("setup_spellchecker"), action); connect(action, &QAction::triggered, this, &ComposerWindow::slotSpellcheckConfig); // Recent actions mRecentAction = new KRecentFilesAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("&Insert Recent Text File"), this); actionCollection()->addAction(QStringLiteral("insert_file_recent"), mRecentAction); connect(mRecentAction, &KRecentFilesAction::urlSelected, this, &ComposerWindow::slotInsertRecentFile); connect(mRecentAction, &KRecentFilesAction::recentListCleared, this, &ComposerWindow::slotRecentListFileClear); const QStringList urls = MessageComposer::MessageComposerSettings::self()->recentUrls(); for (const QString &url : urls) { mRecentAction->addUrl(QUrl(url)); } // print KStandardAction::print(this, &ComposerWindow::slotPrint, actionCollection()); KStandardAction::printPreview(this, &ComposerWindow::slotPrintPreview, actionCollection()); // Send email action action = new QAction(QIcon::fromTheme(QStringLiteral("mail-send")), i18n("&Send Mail"), this); actionCollection()->addAction(QStringLiteral("mail_send"), action); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL | Qt::Key_Return)); connect(action, &QAction::triggered, this, &ComposerWindow::slotSend); // Toggle rich text mMarkupAction = new KToggleAction(i18n("Rich Text Editing"), this); mMarkupAction->setIcon(QIcon::fromTheme(QStringLiteral("preferences-desktop-font"))); mMarkupAction->setIconText(i18n("Rich Text")); mMarkupAction->setToolTip(i18n("Toggle rich text editing mode")); actionCollection()->addAction(QStringLiteral("html"), mMarkupAction); connect(mMarkupAction, &KToggleAction::triggered, this, &ComposerWindow::slotToggleMarkup); mWordWrapAction = new KToggleAction(i18n("&Wordwrap"), this); actionCollection()->addAction(QStringLiteral("wordwrap"), mWordWrapAction); mWordWrapAction->setChecked(MessageComposer::MessageComposerSettings::self()->wordWrap()); connect(mWordWrapAction, &KToggleAction::toggled, this, &ComposerWindow::slotWordWrapToggled); // Encryption action mEncryptAction = new KToggleAction(QIcon::fromTheme(QStringLiteral("document-encrypt")), i18n("&Encrypt Message"), this); mEncryptAction->setIconText(i18n("Encrypt")); actionCollection()->addAction(QStringLiteral("encrypt_message"), mEncryptAction); connect(mEncryptAction, &KToggleAction::toggled, this, &ComposerWindow::slotEncryptionButtonIconUpdate); // Signing action mSignAction = new KToggleAction(QIcon::fromTheme(QStringLiteral("document-sign")), i18n("&Sign Message"), this); mSignAction->setIconText(i18n("Sign")); actionCollection()->addAction(QStringLiteral("sign_message"), mSignAction); connect(mSignAction, &KToggleAction::triggered, this, &ComposerWindow::slotSignToggled); // Append signature mAppendSignature = new QAction(i18n("Append S&ignature"), this); actionCollection()->addAction(QStringLiteral("append_signature"), mAppendSignature); connect(mAppendSignature, &QAction::triggered, mComposerBase->signatureController(), &MessageComposer::SignatureController::appendSignature); // Prepend signature mPrependSignature = new QAction(i18n("Pr&epend Signature"), this); actionCollection()->addAction(QStringLiteral("prepend_signature"), mPrependSignature); connect(mPrependSignature, &QAction::triggered, mComposerBase->signatureController(), &MessageComposer::SignatureController::prependSignature); mInsertSignatureAtCursorPosition = new QAction(i18n("Insert Signature At C&ursor Position"), this); actionCollection()->addAction(QStringLiteral("insert_signature_at_cursor_position"), mInsertSignatureAtCursorPosition); connect(mInsertSignatureAtCursorPosition, &QAction::triggered, mComposerBase->signatureController(), &MessageComposer::SignatureController::insertSignatureAtCursor); action = new QAction(i18n("Paste as Attac&hment"), this); actionCollection()->addAction(QStringLiteral("paste_att"), action); connect(action, &QAction::triggered, this, &ComposerWindow::slotPasteAsAttachment); action = new QAction(i18n("Cl&ean Spaces"), this); actionCollection()->addAction(QStringLiteral("clean_spaces"), action); connect(action, &QAction::triggered, mComposerBase->signatureController(), &MessageComposer::SignatureController::cleanSpace); mRichTextComposer->composerActions()->createActions(actionCollection()); KStandardAction::close(this, &ComposerWindow::close, actionCollection()); KStandardAction::undo(mGlobalAction, &KMComposerGlobalAction::slotUndo, actionCollection()); KStandardAction::redo(mGlobalAction, &KMComposerGlobalAction::slotRedo, actionCollection()); KStandardAction::cut(mGlobalAction, &KMComposerGlobalAction::slotCut, actionCollection()); KStandardAction::copy(mGlobalAction, &KMComposerGlobalAction::slotCopy, actionCollection()); KStandardAction::paste(mGlobalAction, &KMComposerGlobalAction::slotPaste, actionCollection()); mSelectAll = KStandardAction::selectAll(mGlobalAction, &KMComposerGlobalAction::slotMarkAll, actionCollection()); mFindText = KStandardAction::find(mRichTextEditorWidget, &TextCustomEditor::RichTextEditorWidget::slotFind, actionCollection()); mFindNextText = KStandardAction::findNext(mRichTextEditorWidget, &TextCustomEditor::RichTextEditorWidget::slotFindNext, actionCollection()); mReplaceText = KStandardAction::replace(mRichTextEditorWidget, &TextCustomEditor::RichTextEditorWidget::slotReplace, actionCollection()); mComposerBase->attachmentController()->createActions(); createGUI(u"composerui.rc"_s); connect(toolBar(QStringLiteral("htmlToolBar"))->toggleViewAction(), &QAction::toggled, this, &ComposerWindow::htmlToolBarVisibilityChanged); const QList lstLines = mRecipientEditor->lines(); for (KPIM::MultiplyingLine *line : lstLines) { slotRecipientEditorLineAdded(qobject_cast(line)); } } void ComposerWindow::setupStatusBar(QWidget *w) { statusBar()->addWidget(w); mStatusbarLabel = new QLabel(this); mStatusbarLabel->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); statusBar()->addPermanentWidget(mStatusbarLabel); mCursorLineLabel = new QLabel(this); mCursorLineLabel->setTextFormat(Qt::PlainText); mCursorLineLabel->setText(i18nc("Shows the linenumber of the cursor position.", " Line: %1 ", QStringLiteral(" "))); statusBar()->addPermanentWidget(mCursorLineLabel); mCursorColumnLabel = new QLabel(i18n(" Column: %1 ", QStringLiteral(" "))); mCursorColumnLabel->setTextFormat(Qt::PlainText); statusBar()->addPermanentWidget(mCursorColumnLabel); connect(mComposerBase->editor(), &QTextEdit::cursorPositionChanged, this, &ComposerWindow::slotCursorPositionChanged); slotCursorPositionChanged(); } void ComposerWindow::reply(const KMime::Message::Ptr &originalMessage) { MailTemplates::reply(originalMessage, [this](const KMime::Message::Ptr &message) { setMessage(message); Q_EMIT initialized(); }); } - void ComposerWindow::forward(const KMime::Message::Ptr &originalMessage) { MailTemplates::forward(originalMessage, [this](const KMime::Message::Ptr &message) { setMessage(message); Q_EMIT initialized(); }); } void ComposerWindow::setMessage(const KMime::Message::Ptr &msg) { mEdtSubject->setText(msg->subject()->asUnicodeString()); mComposerBase->setMessage(msg); } bool ComposerWindow::isComposerModified() const { - return mComposerBase->editor()->document()->isModified() - || mComposerBase->recipientsEditor()->isModified() - || mEdtSubject->isModified(); + return mComposerBase->editor()->document()->isModified() || mComposerBase->recipientsEditor()->isModified() || mEdtSubject->isModified(); } void ComposerWindow::setModified(bool isModified) { mIsModified = isModified; mComposerBase->editor()->document()->setModified(isModified); if (!isModified) { mComposerBase->recipientsEditor()->clearModified(); mEdtSubject->setModified(false); } } bool ComposerWindow::isModified() const { return mIsModified || isComposerModified(); } void ComposerWindow::setSigning(bool sign, bool setByUser) { const bool wasModified = isModified(); if (setByUser) { setModified(true); } if (!mSignAction->isEnabled()) { sign = false; } // check if the user defined a signing key for the current identity if (sign && !mLastIdentityHasSigningKey) { if (setByUser) { KMessageBox::error(this, i18n("

In order to be able to sign " "this message you first have to " "define the (OpenPGP or S/MIME) signing key " "to use.

" "

Please select the key to use " "in the identity configuration.

" ""), i18nc("@title:window", "Undefined Signing Key")); setModified(wasModified); } sign = false; } // make sure the mSignAction is in the right state mSignAction->setChecked(sign); // mark the attachments for (no) signing - //if (canSignEncryptAttachments()) { + // if (canSignEncryptAttachments()) { // mComposerBase->attachmentModel()->setSignSelected(sign); //} } std::unique_ptr ComposerWindow::fillKeyResolver() { auto keyResolverCore = std::make_unique(true, sign()); keyResolverCore->setMinimumValidity(GpgME::UserID::Unknown); QStringList signingKeys, encryptionKeys; if (cryptoMessageFormat() & Kleo::AnyOpenPGP) { if (!mIdentity.pgpSigningKey().isEmpty()) { signingKeys.push_back(QLatin1String(mIdentity.pgpSigningKey())); } if (!mIdentity.pgpEncryptionKey().isEmpty()) { encryptionKeys.push_back(QLatin1String(mIdentity.pgpEncryptionKey())); } } if (cryptoMessageFormat() & Kleo::AnySMIME) { if (!mIdentity.smimeSigningKey().isEmpty()) { signingKeys.push_back(QLatin1String(mIdentity.smimeSigningKey())); } if (!mIdentity.smimeEncryptionKey().isEmpty()) { encryptionKeys.push_back(QLatin1String(mIdentity.smimeEncryptionKey())); } } keyResolverCore->setSender(mIdentity.fullEmailAddr()); keyResolverCore->setSigningKeys(signingKeys); keyResolverCore->setOverrideKeys({{GpgME::UnknownProtocol, {{keyResolverCore->normalizedSender(), encryptionKeys}}}}); QStringList recipients; const auto lst = mRecipientEditor->lines(); for (auto line : lst) { auto recipient = line->data().dynamicCast(); recipients.push_back(recipient->email()); } keyResolverCore->setRecipients(recipients); qWarning() << recipients; return keyResolverCore; } void ComposerWindow::slotEncryptionButtonIconUpdate() { const auto state = mEncryptAction->isChecked(); auto icon = QIcon::fromTheme(QStringLiteral("document-encrypt")); QString tooltip; if (state) { tooltip = i18nc("@info:tooltip", "Encrypt"); } else { tooltip = i18nc("@info:tooltip", "Not Encrypt"); icon = QIcon::fromTheme(QStringLiteral("document-decrypt")); } if (mAcceptedSolution) { auto overlay = QIcon::fromTheme(QStringLiteral("emblem-added")); if (state) { overlay = QIcon::fromTheme(QStringLiteral("emblem-checked")); } icon = KIconUtils::addOverlay(icon, overlay, Qt::BottomRightCorner); } else { const auto lst = mRecipientEditor->lines(); bool empty = false; if (lst.size() == 1) { const auto line = qobject_cast(lst.first()); if (line->recipientsCount() == 0) { empty = true; } } if (state && !empty) { auto overlay = QIcon::fromTheme(QStringLiteral("emblem-warning")); icon = KIconUtils::addOverlay(icon, overlay, Qt::BottomRightCorner); } } mEncryptAction->setIcon(icon); mEncryptAction->setToolTip(tooltip); } void ComposerWindow::runKeyResolver() { auto keyResolverCore = fillKeyResolver(); auto result = keyResolverCore->resolve(); const auto lst = mRecipientEditor->lines(); if (lst.size() == 1) { const auto line = qobject_cast(lst.first()); if (line->recipientsCount() == 0) { mAcceptedSolution = false; slotEncryptionButtonIconUpdate(); return; } } mAcceptedSolution = result.flags & Kleo::KeyResolverCore::AllResolved; for (auto line_ : lst) { auto line = qobject_cast(line_); Q_ASSERT(line); auto recipient = line->data().dynamicCast(); QString dummy; QString addrSpec; if (KEmailAddress::splitAddress(recipient->email(), dummy, addrSpec, dummy) != KEmailAddress::AddressOk) { addrSpec = recipient->email(); } auto resolvedKeys = result.solution.encryptionKeys[addrSpec]; GpgME::Key key; if (resolvedKeys.size() == 0) { // no key found for recipient // Search for any key, also for not accepted ons, to at least give the user more info. key = Kleo::KeyCache::instance()->findBestByMailBox(addrSpec.toUtf8().constData(), GpgME::UnknownProtocol, Kleo::KeyCache::KeyUsage::Encrypt); key.update(); // We need tofu information for key. recipient->setKey(key); } else { // A key was found for recipient key = resolvedKeys.front(); if (recipient->key().primaryFingerprint() != key.primaryFingerprint()) { key.update(); // We need tofu information for key. recipient->setKey(key); } } annotateRecipientEditorLineWithCryptoInfo(line); if (!key.isNull()) { mExpiryChecker->checkKey(key, Kleo::ExpiryChecker::EncryptionKey); } } slotEncryptionButtonIconUpdate(); } void ComposerWindow::annotateRecipientEditorLineWithCryptoInfo(RecipientLineNG *line) { auto recipient = line->data().dynamicCast(); const auto key = recipient->key(); const auto showCryptoIndicator = true; const auto encrypt = mEncryptAction->isChecked(); const bool showPositiveIcons = showCryptoIndicator && encrypt; const bool showAllIcons = showCryptoIndicator && encrypt; QString dummy; QString addrSpec; bool invalidEmail = false; if (KEmailAddress::splitAddress(recipient->email(), dummy, addrSpec, dummy) != KEmailAddress::AddressOk) { invalidEmail = true; addrSpec = recipient->email(); } if (key.isNull()) { recipient->setEncryptionAction(Kleo::Impossible); if (showAllIcons && !invalidEmail) { const auto icon = QIcon::fromTheme(QStringLiteral("emblem-error")); line->setIcon(icon, i18nc("@info:tooltip", "No key found for the recipient.")); } else { line->setIcon(QIcon()); } line->setProperty("keyStatus", invalidEmail ? InProgress : NoKey); return; } CryptoKeyState keyState = KeyOk; if (recipient->encryptionAction() != Kleo::DoIt) { recipient->setEncryptionAction(Kleo::DoIt); } QString tooltip; const auto uids = key.userIDs(); const auto _uid = findSendersUid(addrSpec.toStdString(), uids); GpgME::UserID uid; if (_uid == uids.cend()) { uid = key.userID(0); } else { uid = *_uid; } const auto trustLevel = Kleo::trustLevel(uid); switch (trustLevel) { case Kleo::Level0: if (uid.tofuInfo().isNull()) { tooltip = i18nc("@info:tooltip", "The encryption key is not trusted. It hasn't enough validity. " "You can sign the key, if you communicated the fingerprint by another channel. " "Click the icon for details."); keyState = NoTrusted; } else { switch (uid.tofuInfo().validity()) { case GpgME::TofuInfo::NoHistory: tooltip = i18nc("@info:tooltip", "The encryption key is not trusted. " "It hasn't been used anywhere to guarantee it belongs to the stated person. " "By using the key will be trusted more. " "Or you can sign the key, if you communicated the fingerprint by another channel. " "Click the icon for details."); break; case GpgME::TofuInfo::Conflict: tooltip = i18nc("@info:tooltip", "The encryption key is not trusted. It has conflicting TOFU data. " "Click the icon for details."); keyState = NoKey; break; case GpgME::TofuInfo::ValidityUnknown: tooltip = i18nc("@info:tooltip", "The encryption key is not trusted. It has unknown validity in TOFU data. " "Click the icon for details."); keyState = NoKey; break; default: tooltip = i18nc("@info:tooltip", "The encryption key is not trusted. The key is marked as bad. " "Click the icon for details."); keyState = NoKey; } } break; case Kleo::Level1: tooltip = i18nc("@info:tooltip", "The encryption key is only marginally trusted and hasn't been used enough time to guarantee it belongs to the stated person. " "By using the key will be trusted more. " "Or you can sign the key, if you communicated the fingerprint by another channel. " "Click the icon for details."); break; case Kleo::Level2: if (uid.tofuInfo().isNull()) { tooltip = i18nc("@info:tooltip", "The encryption key is only marginally trusted. " "You can sign the key, if you communicated the fingerprint by another channel. " "Click the icon for details."); } else { - tooltip = - i18nc("@info:tooltip", - "The encryption key is only marginally trusted, but has been used enough times to be very likely controlled by the stated person. " - "By using the key will be trusted more. " - "Or you can sign the key, if you communicated the fingerprint by another channel. " - "Click the icon for details."); + tooltip = i18nc("@info:tooltip", + "The encryption key is only marginally trusted, but has been used enough times to be very likely controlled by the stated person. " + "By using the key will be trusted more. " + "Or you can sign the key, if you communicated the fingerprint by another channel. " + "Click the icon for details."); } break; case Kleo::Level3: tooltip = i18nc("@info:tooltip", "The encryption key is fully trusted. You can raise the security level, by signing the key. " "Click the icon for details."); break; case Kleo::Level4: tooltip = i18nc("@info:tooltip", "The encryption key is ultimately trusted or is signed by another ultimately trusted key. " "Click the icon for details."); break; default: Q_UNREACHABLE(); } // Ensure the tooltips are word wrapped tooltip = u"
"_s + tooltip + u"
"_s; if (keyState == NoKey) { mAcceptedSolution = false; if (showAllIcons) { line->setIcon(QIcon::fromTheme(QStringLiteral("emblem-error")), tooltip); } else { line->setIcon(QIcon()); } } else if (trustLevel == Kleo::Level0 && encrypt) { if (keyState == NoTrusted) { line->setIcon(QIcon::fromTheme(QStringLiteral("emblem-question")), tooltip); } else { line->setIcon(QIcon::fromTheme(QStringLiteral("emblem-warning")), tooltip); } } else if (showPositiveIcons) { // Magically, the icon name maps precisely to each trust level // line->setIcon(QIcon::fromTheme(QStringLiteral("gpg-key-trust-level-%1").arg(trustLevel)), tooltip); line->setIcon(QIcon::fromTheme(QStringLiteral("emblem-success")), tooltip); } else { line->setIcon(QIcon()); } if (line->property("keyStatus") != keyState) { line->setProperty("keyStatus", keyState); } } void ComposerWindow::slotSignToggled(bool on) { setSigning(on, true); } bool ComposerWindow::sign() const { return mSignAction->isChecked(); } void ComposerWindow::slotSend() { - mComposerBase->setFrom(mFrom); mComposerBase->setSubject(mEdtSubject->text()); if (mComposerBase->to().isEmpty()) { if (mComposerBase->cc().isEmpty() && mComposerBase->bcc().isEmpty()) { KMessageBox::information(this, i18n("You must specify at least one receiver, " "either in the To: field or as CC or as BCC.")); return; } else { const int rc = KMessageBox::questionTwoActions(this, i18n("To: field is empty. " "Send message anyway?"), i18nc("@title:window", "No To: specified"), KGuiItem(i18n("S&end as Is"), QLatin1String("mail-send")), KGuiItem(i18n("&Specify the To field"), QLatin1String("edit-rename"))); if (rc == KMessageBox::ButtonCode::SecondaryAction) { return; } } } - if (mComposerBase->subject().isEmpty()) { mEdtSubject->setFocus(); const int rc = KMessageBox::questionTwoActions(this, i18n("You did not specify a subject. " "Send message anyway?"), i18nc("@title:window", "No Subject Specified"), KGuiItem(i18n("S&end as Is"), QStringLiteral("mail-send")), KGuiItem(i18n("&Specify the Subject"), QStringLiteral("edit-rename"))); if (rc == KMessageBox::ButtonCode::SecondaryAction) { return; } } KCursorSaver saver(Qt::WaitCursor); const bool encrypt = mEncryptAction->isChecked(); - mComposerBase->setCryptoOptions( - sign(), - encrypt, - cryptoMessageFormat()); + mComposerBase->setCryptoOptions(sign(), encrypt, cryptoMessageFormat()); mComposerBase->send(); } void ComposerWindow::changeCryptoAction() { if (!QGpgME::openpgp() && !QGpgME::smime()) { // no crypto whatsoever mEncryptAction->setEnabled(false); mSignAction->setEnabled(false); setSigning(false); } else { setSigning(true); mEncryptAction->setChecked(true); } } void ComposerWindow::slotToggleMarkup() { htmlToolBarVisibilityChanged(mMarkupAction->isChecked()); } void ComposerWindow::htmlToolBarVisibilityChanged(bool visible) { if (visible) { enableHtml(); } else { disableHtml(LetUserConfirm); } } void ComposerWindow::enableHtml() { if (mForceDisableHtml) { disableHtml(NoConfirmationNeeded); return; } mRichTextComposer->activateRichText(); if (!toolBar(QStringLiteral("htmlToolBar"))->isVisible()) { // Use singleshot, as we we might actually be called from a slot that wanted to disable the // toolbar (but the messagebox in disableHtml() prevented that and called us). // The toolbar can't correctly deal with being enabled right in a slot called from the "disabled" // signal, so wait one event loop run for that. QTimer::singleShot(0, toolBar(QStringLiteral("htmlToolBar")), &QWidget::show); } if (!mMarkupAction->isChecked()) { mMarkupAction->setChecked(true); } mRichTextComposer->composerActions()->updateActionStates(); mRichTextComposer->composerActions()->setActionsEnabled(true); } void ComposerWindow::disableHtml(Confirmation confirmation) { bool forcePlainTextMarkup = false; if (confirmation == LetUserConfirm && mRichTextComposer->composerControler()->isFormattingUsed()) { int choice = KMessageBox::warningTwoActionsCancel(this, i18n("Turning HTML mode off " "will cause the text to lose the formatting. Are you sure?"), i18n("Lose the formatting?"), KGuiItem(i18n("Lose Formatting")), KGuiItem(i18n("Add Markup Plain Text")), KStandardGuiItem::cancel(), QStringLiteral("LoseFormattingWarning")); switch (choice) { case KMessageBox::Cancel: enableHtml(); return; case KMessageBox::ButtonCode::SecondaryAction: forcePlainTextMarkup = true; break; case KMessageBox::ButtonCode::PrimaryAction: break; } } mRichTextComposer->forcePlainTextMarkup(forcePlainTextMarkup); mRichTextComposer->switchToPlainText(); mRichTextComposer->composerActions()->setActionsEnabled(false); if (toolBar(QStringLiteral("htmlToolBar"))->isVisible()) { // See the comment in enableHtml() why we use a singleshot timer, similar situation here. QTimer::singleShot(0, toolBar(QStringLiteral("htmlToolBar")), &QWidget::hide); } if (mMarkupAction->isChecked()) { mMarkupAction->setChecked(false); } } inline Kleo::chrono::days encryptOwnKeyNearExpiryWarningThresholdInDays() { const int num = 30; return Kleo::chrono::days{qMax(1, num)}; } inline Kleo::chrono::days encryptKeyNearExpiryWarningThresholdInDays() { const int num = 14; return Kleo::chrono::days{qMax(1, num)}; } inline Kleo::chrono::days encryptRootCertNearExpiryWarningThresholdInDays() { const int num = 14; return Kleo::chrono::days{qMax(1, num)}; } inline Kleo::chrono::days encryptChainCertNearExpiryWarningThresholdInDays() { const int num = 14; return Kleo::chrono::days{qMax(1, num)}; } std::shared_ptr ComposerWindow::expiryChecker() { if (!mExpiryChecker) { mExpiryChecker.reset(new Kleo::ExpiryChecker{Kleo::ExpiryCheckerSettings{encryptOwnKeyNearExpiryWarningThresholdInDays(), encryptKeyNearExpiryWarningThresholdInDays(), encryptRootCertNearExpiryWarningThresholdInDays(), encryptChainCertNearExpiryWarningThresholdInDays()}}); } return mExpiryChecker; } Kleo::CryptoMessageFormat ComposerWindow::cryptoMessageFormat() const { return Kleo::AutoFormat; } void ComposerWindow::slotEditIdentity() { QPointer dlg = new KMail::IdentityDialog(); dlg->setAttribute(Qt::WA_DeleteOnClose); dlg->setIdentity(mIdentity); dlg->open(); connect(dlg, &KMail::IdentityDialog::accepted, this, [dlg, this]() { dlg->updateIdentity(mIdentity); IdentityManager::self().updateIdentity(mIdentity); IdentityManager::self().writeConfig(); slotIdentityChanged(); }); } void ComposerWindow::slotIdentityChanged() { mComposerBase->setIdentity(mIdentity); mLastIdentityHasSigningKey = !mIdentity.pgpSigningKey().isEmpty() || !mIdentity.smimeSigningKey().isEmpty(); mLastIdentityHasEncryptionKey = !mIdentity.pgpEncryptionKey().isEmpty() || !mIdentity.smimeEncryptionKey().isEmpty(); mComposerBase->signatureController()->setIdentity(mIdentity); mComposerBase->editor()->setAutocorrectionLanguage(mIdentity.autocorrectionLanguage()); mComposerBase->dictionary()->setCurrentByDictionaryName(mIdentity.dictionary()); mComposerBase->editor()->setSpellCheckingLanguage(mComposerBase->dictionary()->currentDictionary()); bool bPGPEncryptionKey = !mIdentity.pgpEncryptionKey().isEmpty(); bool bPGPSigningKey = !mIdentity.pgpSigningKey().isEmpty(); bool bSMIMEEncryptionKey = !mIdentity.smimeEncryptionKey().isEmpty(); bool bSMIMESigningKey = !mIdentity.smimeSigningKey().isEmpty(); if (cryptoMessageFormat() & Kleo::AnyOpenPGP) { if (bPGPEncryptionKey) { auto const key = mKeyCache->findByKeyIDOrFingerprint(mIdentity.pgpEncryptionKey().constData()); if (key.isNull() || !key.canEncrypt()) { bPGPEncryptionKey = false; } } if (bPGPSigningKey) { auto const key = mKeyCache->findByKeyIDOrFingerprint(mIdentity.pgpSigningKey().constData()); if (key.isNull() || !key.canSign()) { bPGPSigningKey = false; } } } else { bPGPEncryptionKey = false; bPGPSigningKey = false; } if (cryptoMessageFormat() & Kleo::AnySMIME) { if (bSMIMEEncryptionKey) { auto const key = mKeyCache->findByKeyIDOrFingerprint(mIdentity.smimeEncryptionKey().constData()); if (key.isNull() || !key.canEncrypt()) { bSMIMEEncryptionKey = false; } } if (bSMIMESigningKey) { auto const key = mKeyCache->findByKeyIDOrFingerprint(mIdentity.smimeSigningKey().constData()); if (key.isNull() || !key.canSign()) { bSMIMESigningKey = false; } } } else { bSMIMEEncryptionKey = false; bSMIMESigningKey = false; } bool bNewIdentityHasSigningKey = bPGPSigningKey || bSMIMESigningKey; bool bNewIdentityHasEncryptionKey = bPGPEncryptionKey || bSMIMEEncryptionKey; if (!mKeyCache->initialized()) { // We need to start key listing on our own othweise KMail will crash and we want to wait till the cache is populated. mKeyCache->startKeyListing(); connect(mKeyCache.get(), &Kleo::KeyCache::keyListingDone, this, [this]() { checkOwnKeyExpiry(mIdentity); runKeyResolver(); }); } else { checkOwnKeyExpiry(mIdentity); } // save the state of the sign and encrypt button if (!bNewIdentityHasEncryptionKey && mLastIdentityHasEncryptionKey) { mLastEncryptActionState = mEncryptAction->isChecked(); } mSignAction->setEnabled(bNewIdentityHasSigningKey); if (!bNewIdentityHasSigningKey && mLastIdentityHasSigningKey) { mLastSignActionState = sign(); setSigning(false); } // restore the last state of the sign and encrypt button if (bNewIdentityHasSigningKey && !mLastIdentityHasSigningKey) { setSigning(mLastSignActionState); } mLastIdentityHasSigningKey = bNewIdentityHasSigningKey; mLastIdentityHasEncryptionKey = bNewIdentityHasEncryptionKey; const KIdentityManagementCore::Signature sig = const_cast(mIdentity).signature(); bool isEnabledSignature = sig.isEnabledSignature(); mAppendSignature->setEnabled(isEnabledSignature); mPrependSignature->setEnabled(isEnabledSignature); mInsertSignatureAtCursorPosition->setEnabled(isEnabledSignature); changeCryptoAction(); Q_EMIT identityChanged(); } void ComposerWindow::checkOwnKeyExpiry(const KIdentityManagementCore::Identity &ident) { mNearExpiryWarning->clearInfo(); mNearExpiryWarning->hide(); if (cryptoMessageFormat() & Kleo::AnyOpenPGP) { if (!ident.pgpEncryptionKey().isEmpty()) { auto const key = mKeyCache->findByKeyIDOrFingerprint(ident.pgpEncryptionKey().constData()); if (key.isNull() || !key.canEncrypt()) { mNearExpiryWarning->addInfo(i18nc("The argument is as PGP fingerprint", "Your selected PGP key (%1) doesn't exist in your keyring or is not suitable for encryption.", QString::fromUtf8(ident.pgpEncryptionKey()))); mNearExpiryWarning->setWarning(true); mNearExpiryWarning->show(); } else { mComposerBase->expiryChecker()->checkKey(key, Kleo::ExpiryChecker::OwnEncryptionKey); } } if (!ident.pgpSigningKey().isEmpty()) { if (ident.pgpSigningKey() != ident.pgpEncryptionKey()) { auto const key = mKeyCache->findByKeyIDOrFingerprint(ident.pgpSigningKey().constData()); if (key.isNull() || !key.canSign()) { mNearExpiryWarning->addInfo(i18nc("The argument is as PGP fingerprint", "Your selected PGP signing key (%1) doesn't exist in your keyring or is not suitable for signing.", QString::fromUtf8(ident.pgpSigningKey()))); mNearExpiryWarning->setWarning(true); mNearExpiryWarning->show(); } else { mComposerBase->expiryChecker()->checkKey(key, Kleo::ExpiryChecker::OwnSigningKey); } } } } if (cryptoMessageFormat() & Kleo::AnySMIME) { if (!ident.smimeEncryptionKey().isEmpty()) { auto const key = mKeyCache->findByKeyIDOrFingerprint(ident.smimeEncryptionKey().constData()); if (key.isNull() || !key.canEncrypt()) { mNearExpiryWarning->addInfo(i18nc("The argument is as SMIME fingerprint", "Your selected SMIME key (%1) doesn't exist in your keyring or is not suitable for encryption.", QString::fromUtf8(ident.smimeEncryptionKey()))); mNearExpiryWarning->setWarning(true); mNearExpiryWarning->show(); } else { mComposerBase->expiryChecker()->checkKey(key, Kleo::ExpiryChecker::OwnEncryptionKey); } } if (!ident.smimeSigningKey().isEmpty()) { if (ident.smimeSigningKey() != ident.smimeEncryptionKey()) { auto const key = mKeyCache->findByKeyIDOrFingerprint(ident.smimeSigningKey().constData()); if (key.isNull() || !key.canSign()) { mNearExpiryWarning->addInfo(i18nc("The argument is as SMIME fingerprint", "Your selected SMIME signing key (%1) doesn't exist in your keyring or is not suitable for signing.", QString::fromUtf8(ident.smimeSigningKey()))); mNearExpiryWarning->setWarning(true); mNearExpiryWarning->show(); } else { mComposerBase->expiryChecker()->checkKey(key, Kleo::ExpiryChecker::OwnSigningKey); } } } } } void ComposerWindow::slotCursorPositionChanged() { // Change Line/Column info in status bar const int line = mComposerBase->editor()->linePosition() + 1; const int col = mComposerBase->editor()->columnNumber() + 1; QString temp = i18nc("Shows the linenumber of the cursor position.", " Line: %1 ", line); mCursorLineLabel->setText(temp); temp = i18n(" Column: %1 ", col); mCursorColumnLabel->setText(temp); // Show link target in status bar if (mComposerBase->editor()->textCursor().charFormat().isAnchor()) { const QString text = mComposerBase->editor()->composerControler()->currentLinkText() + QLatin1String(" -> ") + mComposerBase->editor()->composerControler()->currentLinkUrl(); mStatusbarLabel->setText(text); } else { mStatusbarLabel->clear(); } } KIdentityManagementCore::Identity ComposerWindow::identity() const { return mIdentity; } QString ComposerWindow::subject() const { return mEdtSubject->text(); } QString ComposerWindow::content() const { return mComposerBase->editor()->toCleanHtml(); } RecipientsEditor *ComposerWindow::recipientsEditor() const { return mRecipientEditor; } void ComposerWindow::addAttachment(const QList &infos, bool showWarning) { QStringList lst; for (const AttachmentInfo &info : infos) { if (showWarning) { lst.append(info.url.toDisplayString()); } mComposerBase->addAttachment(info.url, info.comment, false); } if (showWarning) { // TODO // mAttachmentFromExternalMissing->setAttachmentNames(lst); // mAttachmentFromExternalMissing->animatedShow(); } } void ComposerWindow::addAttachment(const QString &name, - KMime::Headers::contentEncoding cte, - const QString &charset, - const QByteArray &data, - const QByteArray &mimeType) + KMime::Headers::contentEncoding cte, + const QString &charset, + const QByteArray &data, + const QByteArray &mimeType) { Q_UNUSED(cte) mComposerBase->addAttachment(name, name, charset, data, mimeType); } void ComposerWindow::insertUrls(const QMimeData *source, const QList &urlList) { QStringList urlAdded; for (const QUrl &url : urlList) { QString urlStr; if (url.scheme() == QLatin1String("mailto")) { urlStr = KEmailAddress::decodeMailtoUrl(url); } else { urlStr = url.toDisplayString(); // Workaround #346370 if (urlStr.isEmpty()) { urlStr = source->text(); } } if (!urlAdded.contains(urlStr)) { mComposerBase->editor()->composerControler()->insertLink(urlStr); urlAdded.append(urlStr); } } } bool ComposerWindow::insertFromMimeData(const QMimeData *source, bool forceAttachment) { // If this is a PNG image, either add it as an attachment or as an inline image if (source->hasHtml() && mComposerBase->editor()->textMode() == MessageComposer::RichTextComposerNg::Rich) { const QString html = QString::fromUtf8(source->data(QStringLiteral("text/html"))); mComposerBase->editor()->insertHtml(html); return true; } else if (source->hasHtml() && (mComposerBase->editor()->textMode() == MessageComposer::RichTextComposerNg::Plain) && source->hasText() && !forceAttachment) { mComposerBase->editor()->insertPlainText(source->text()); return true; } else if (source->hasImage() && source->hasFormat(QStringLiteral("image/png"))) { // Get the image data before showing the dialog, since that processes events which can delete // the QMimeData object behind our back const QByteArray imageData = source->data(QStringLiteral("image/png")); if (imageData.isEmpty()) { return true; } if (!forceAttachment) { if (mComposerBase->editor()->textMode() == MessageComposer::RichTextComposerNg::Rich /*&& mComposerBase->editor()->isEnableImageActions() Necessary ?*/) { auto image = qvariant_cast(source->imageData()); QFileInfo fi(source->text()); QMenu menu(this); const QAction *addAsInlineImageAction = menu.addAction(i18n("Add as &Inline Image")); menu.addAction(i18n("Add as &Attachment")); const QAction *selectedAction = menu.exec(QCursor::pos()); if (selectedAction == addAsInlineImageAction) { // Let the textedit from kdepimlibs handle inline images mComposerBase->editor()->composerControler()->composerImages()->insertImage(image, fi); return true; } else if (!selectedAction) { return true; } // else fall through } } // Ok, when we reached this point, the user wants to add the image as an attachment. // Ask for the filename first. bool ok; QString attName = QInputDialog::getText(this, i18n("KMail"), i18n("Name of the attachment:"), QLineEdit::Normal, QString(), &ok); if (!ok) { return true; } attName = attName.trimmed(); if (attName.isEmpty()) { KMessageBox::error(this, i18n("Attachment name can't be empty"), i18nc("@title:window", "Invalid Attachment Name")); return true; } addAttachment(attName, KMime::Headers::CEbase64, QString(), imageData, "image/png"); return true; } // If this is a URL list, add those files as attachments or text // but do not offer this if we are pasting plain text containing an url, e.g. from a browser const QList urlList = source->urls(); if (!urlList.isEmpty()) { // Search if it's message items. bool allLocalURLs = true; for (const QUrl &url : urlList) { if (!url.isLocalFile()) { allLocalURLs = false; } } if (allLocalURLs || forceAttachment) { QList infoList; infoList.reserve(urlList.count()); for (const QUrl &url : urlList) { AttachmentInfo info; info.url = url; infoList.append(info); } addAttachment(infoList, false); } else { QMenu p; const int sizeUrl(urlList.size()); const QAction *addAsTextAction = p.addAction(i18np("Add URL into Message", "Add URLs into Message", sizeUrl)); const QAction *addAsAttachmentAction = p.addAction(i18np("Add File as &Attachment", "Add Files as &Attachment", sizeUrl)); const QAction *selectedAction = p.exec(QCursor::pos()); if (selectedAction == addAsTextAction) { insertUrls(source, urlList); } else if (selectedAction == addAsAttachmentAction) { QList infoList; for (const QUrl &url : urlList) { if (url.isValid()) { AttachmentInfo info; info.url = url; infoList.append(info); } } addAttachment(infoList, false); } } return true; } return false; } void ComposerWindow::slotSaveDraft() { mComposerBase->slotSaveDraft(); } void ComposerWindow::slotSaveAsFile() { auto job = new SaveAsFileJob(this); job->setParentWidget(this); job->setHtmlMode(mComposerBase->editor()->textMode() == MessageComposer::RichTextComposerNg::Rich); job->setTextDocument(mComposerBase->editor()->document()); job->start(); } QUrl ComposerWindow::insertFile() { const auto fileName = QFileDialog::getOpenFileName(this, i18nc("@title:window", "Insert File")); return QUrl::fromUserInput(fileName); } void ComposerWindow::slotInsertFile() { const QUrl u = insertFile(); if (u.isEmpty()) { return; } mRecentAction->addUrl(u); // Prevent race condition updating list when multiple composers are open { QUrlQuery query(u); QStringList urls = MessageComposer::MessageComposerSettings::self()->recentUrls(); // Prevent config file from growing without bound // Would be nicer to get this constant from KRecentFilesAction const int mMaxRecentFiles = 30; while (urls.count() > mMaxRecentFiles) { urls.removeLast(); } urls.prepend(u.toDisplayString()); MessageComposer::MessageComposerSettings::self()->setRecentUrls(urls); MessageComposer::MessageComposerSettings::self()->save(); } slotInsertRecentFile(u); } void ComposerWindow::slotRecentListFileClear() { MessageComposer::MessageComposerSettings::self()->setRecentUrls({}); MessageComposer::MessageComposerSettings::self()->save(); } void ComposerWindow::slotInsertRecentFile(const QUrl &u) { if (u.fileName().isEmpty()) { return; } auto job = new MessageComposer::InsertTextFileJob(mComposerBase->editor(), u); job->start(); } - void ComposerWindow::slotPrint() { QPrinter printer; QPrintDialog dialog(&printer, this); dialog.setWindowTitle(i18nc("@title:window", "Print Document")); if (dialog.exec() != QDialog::Accepted) return; printInternal(&printer); } void ComposerWindow::slotPrintPreview() { auto dialog = new QPrintPreviewDialog(this); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->resize(800, 750); dialog->setWindowTitle(i18nc("@title:window", "Print Document")); QObject::connect(dialog, &QPrintPreviewDialog::paintRequested, this, [this](QPrinter *printer) { printInternal(printer); }); dialog->open(); } void ComposerWindow::printInternal(QPrinter *printer) { mComposerBase->setFrom(mFrom); mComposerBase->setSubject(mEdtSubject->text()); mComposerBase->generateMessage([printer](const QList &messages) { if (messages.isEmpty()) { return; } MimeTreeParser::Widgets::MessageViewer messageViewer; messageViewer.setMessage(messages[0]); QPainter painter; painter.begin(printer); const auto pageLayout = printer->pageLayout(); const auto pageRect = pageLayout.paintRectPixels(printer->resolution()); const double xscale = pageRect.width() / double(messageViewer.width()); const double yscale = pageRect.height() / double(messageViewer.height()); const double scale = qMin(qMin(xscale, yscale), 1.); painter.translate(pageRect.x(), pageRect.y()); painter.scale(scale, scale); messageViewer.print(&painter, pageRect.width()); }); } - void ComposerWindow::slotPasteAsAttachment() { const QMimeData *mimeData = QApplication::clipboard()->mimeData(); if (!mimeData) { return; } if (insertFromMimeData(mimeData, true)) { return; } if (mimeData->hasText()) { bool ok; const QString attName = QInputDialog::getText(this, i18n("Insert clipboard text as attachment"), i18n("Name of the attachment:"), QLineEdit::Normal, QString(), &ok); if (ok) { mComposerBase->addAttachment(attName, attName, QStringLiteral("utf-8"), QApplication::clipboard()->text().toUtf8(), "text/plain"); } return; } } void ComposerWindow::slotWordWrapToggled(bool on) { if (on) { mComposerBase->editor()->enableWordWrap(validateLineWrapWidth()); } else { disableWordWrap(); } } int ComposerWindow::validateLineWrapWidth() const { int lineWrap = MessageComposer::MessageComposerSettings::self()->lineWrapWidth(); if ((lineWrap == 0) || (lineWrap > 78)) { lineWrap = 78; } else if (lineWrap < 30) { lineWrap = 30; } return lineWrap; } void ComposerWindow::disableWordWrap() { mComposerBase->editor()->disableWordWrap(); } void ComposerWindow::slotAutoSpellCheckingToggled(bool enabled) { mAutoSpellCheckingAction->setChecked(enabled); if (mComposerBase->editor()->checkSpellingEnabled() != enabled) { mComposerBase->editor()->setCheckSpellingEnabled(enabled); } - //mStatusBarLabelSpellCheckingChangeMode->setToggleMode(enabled); + // mStatusBarLabelSpellCheckingChangeMode->setToggleMode(enabled); } void ComposerWindow::slotSpellcheckConfig() { QPointer dialog = new SpellCheckerConfigDialog(this); if (!mComposerBase->editor()->spellCheckingLanguage().isEmpty()) { dialog->setLanguage(mComposerBase->editor()->spellCheckingLanguage()); } if (dialog->exec()) { mComposerBase->editor()->setSpellCheckingLanguage(dialog->language()); } delete dialog; } void ComposerWindow::closeEvent(QCloseEvent *event) { event->ignore(); ComposerWindowFactory::self().clear(this); } bool ComposerWindow::queryClose() { if (isModified()) { const QString savebut = i18n("&Save as Draft"); const QString savetext = i18n("Save this message encrypted in your drafts folder. It can then be edited and sent at a later time."); const int rc = KMessageBox::warningTwoActionsCancel(this, i18n("Do you want to save the message for later or discard it?"), i18nc("@title:window", "Close Composer"), KGuiItem(savebut, QStringLiteral("document-save"), QString(), savetext), KStandardGuiItem::discard(), KStandardGuiItem::cancel()); if (rc == KMessageBox::Cancel) { return false; } else if (rc == KMessageBox::ButtonCode::PrimaryAction) { // doSend will close the window. Just return false from this method slotSaveDraft(); return false; } // else fall through: return true } mComposerBase->cleanupAutoSave(); return true; } void ComposerWindow::slotRecipientEditorLineAdded(RecipientLineNG *line) { connect(line, &RecipientLineNG::countChanged, this, [this, line]() { slotRecipientAdded(line); }); connect(line, &RecipientLineNG::iconClicked, this, [this, line]() { slotRecipientLineIconClicked(line); }); connect(line, &RecipientLineNG::destroyed, this, &ComposerWindow::slotRecipientEditorFocusChanged, Qt::QueuedConnection); - connect(line, &RecipientLineNG::activeChanged, this, [this, line]() { - slotRecipientFocusLost(line); - }, Qt::QueuedConnection); + connect( + line, + &RecipientLineNG::activeChanged, + this, + [this, line]() { + slotRecipientFocusLost(line); + }, + Qt::QueuedConnection); slotRecipientEditorFocusChanged(); } void ComposerWindow::slotRecipientLineIconClicked(RecipientLineNG *line) { const auto recipient = line->data().dynamicCast(); if (!recipient->key().isNull()) { const QString exec = QStandardPaths::findExecutable(QStringLiteral("kleopatra")); if (exec.isEmpty() || !QProcess::startDetached(exec, {QStringLiteral("--query"), QString::fromLatin1(recipient->key().primaryFingerprint()), QStringLiteral("--parent-windowid"), QString::number(winId())})) { qCWarning(EDITOR_LOG) << "Unable to execute kleopatra"; } return; } const auto msg = i18nc( "if in your language something like " "'certificate(s)' is not possible please " "use the plural in the translation", "No valid and trusted encryption certificate was " "found for \"%1\".

" "Select the certificate(s) which should " "be used for this recipient. If there is no suitable certificate in the list " "you can also search for external certificates by clicking the button: " "search for external certificates.
", recipient->name().isEmpty() ? recipient->email() : recipient->name()); const bool opgp = containsOpenPGP(cryptoMessageFormat()); const bool x509 = containsSMIME(cryptoMessageFormat()); QPointer dlg = new Kleo::KeySelectionDialog( i18n("Encryption Key Selection"), msg, recipient->email(), {}, Kleo::KeySelectionDialog::ValidEncryptionKeys | (opgp ? Kleo::KeySelectionDialog::OpenPGPKeys : 0) | (x509 ? Kleo::KeySelectionDialog::SMIMEKeys : 0), false, // multi-selection false); // "remember choice" box; dlg->open(); connect(dlg, &QDialog::accepted, this, [dlg, recipient, line, this]() { auto key = dlg->selectedKey(); key.update(); // We need tofu information for key. recipient->setKey(key); annotateRecipientEditorLineWithCryptoInfo(line); }); } void ComposerWindow::slotRecipientEditorFocusChanged() { if (!mEncryptAction->isChecked()) { return; } if (mKeyCache->initialized()) { mRunKeyResolverTimer->stop(); runKeyResolver(); } } void ComposerWindow::slotRecipientAdded(RecipientLineNG *line) { if (line->recipientsCount() == 0) { return; } if (!mKeyCache->initialized()) { if (line->property("keyLookupJob").toBool()) { return; } line->setProperty("keyLookupJob", true); // We need to start key listing on our own othweise KMail will crash and we want to wait till the cache is populated. connect(mKeyCache.get(), &Kleo::KeyCache::keyListingDone, this, [this, line]() { slotRecipientAdded(line); }); return; } if (mKeyCache->initialized()) { mRunKeyResolverTimer->start(); } } void ComposerWindow::slotRecipientFocusLost(RecipientLineNG *line) { if (line->recipientsCount() == 0) { return; } if (mKeyCache->initialized()) { mRunKeyResolverTimer->start(); } } - void ComposerWindow::slotSendSuccessful() { setModified(false); mComposerBase->cleanupAutoSave(); close(); } diff --git a/client/editor/composerwindow.h b/client/editor/composerwindow.h index 7160141..a62822d 100644 --- a/client/editor/composerwindow.h +++ b/client/editor/composerwindow.h @@ -1,229 +1,229 @@ // SPDX-FileCopyrightText: 2023 g10 code Gmbh // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #pragma once // Qt includes #include // KDE includes +#include "identity/identity.h" +#include +#include #include #include #include -#include -#include -#include "identity/identity.h" // App includes #include "composerwindowfactory.h" class QSplitter; class QLabel; class QPrinter; class QGridLayout; class QLineEdit; class QPushButton; class KLineEdit; class RecipientsEditor; class KToggleAction; class RecipientLineNG; class NearExpiryWarning; class IdentityCombo; class KMComposerGlobalAction; class KRecentFilesAction; namespace KPIMTextEdit { class RichTextComposerWidget; class RichTextComposer; } namespace TextCustomEditor { class RichTextEditorWidget; } -namespace MessageComposer { +namespace MessageComposer +{ class ComposerViewBase; class RichTextComposerNg; } namespace Kleo { class KeyResolverCore; class ExpiryChecker; } class ComposerWindow : public KXmlGuiWindow { Q_OBJECT enum Confirmation { LetUserConfirm, NoConfirmationNeeded, }; public: struct AttachmentInfo { QString comment; QUrl url; }; /// The identity assigned to this message. KIdentityManagementCore::Identity identity() const; /// The subject of the message. QString subject() const; /// The recipients of the message. RecipientsEditor *recipientsEditor() const; /// The content of the message. QString content() const; void addAttachment(const QList &infos, bool showWarning); void reply(const KMime::Message::Ptr &message); void forward(const KMime::Message::Ptr &message); void setMessage(const KMime::Message::Ptr &message); - private Q_SLOTS: void slotSend(); void slotToggleMarkup(); void slotSignToggled(bool on); void slotSaveDraft(); void slotSaveAsFile(); void slotInsertFile(); void slotEncryptionButtonIconUpdate(); void slotEditIdentity(); void slotIdentityChanged(); void slotPrint(); void slotPrintPreview(); void slotWordWrapToggled(bool on); void slotAutoSpellCheckingToggled(bool enabled); void slotSpellcheckConfig(); void printInternal(QPrinter *printer); void enableHtml(); void slotPasteAsAttachment(); void disableHtml(Confirmation confirmation); void slotCursorPositionChanged(); void slotInsertRecentFile(const QUrl &u); void slotRecentListFileClear(); void slotRecipientEditorFocusChanged(); void slotRecipientAdded(RecipientLineNG *line); void slotRecipientFocusLost(RecipientLineNG *line); void slotRecipientEditorLineAdded(RecipientLineNG *line); void slotRecipientLineIconClicked(RecipientLineNG *line); void slotSendSuccessful(); void insertUrls(const QMimeData *source, const QList &urlList); bool insertFromMimeData(const QMimeData *source, bool forceAttachment); QUrl insertFile(); void addAttachment(const QString &name, KMime::Headers::contentEncoding cte, const QString &charset, const QByteArray &data, const QByteArray &mimeType); /// Set whether the message will be signed. void setSigning(bool sign, bool setByUser = false); /// Set whether the message should be treated as modified or not. void setModified(bool modified); std::shared_ptr expiryChecker(); Q_SIGNALS: void identityChanged(); void initialized(); protected: friend ComposerWindowFactory; explicit ComposerWindow(const QString &fromAddress, const QString &name, const QByteArray &bearerToken, QWidget *parent = nullptr); void reset(const QString &fromAddress, const QString &name, const QByteArray &bearerToken); void closeEvent(QCloseEvent *event) override; private: enum CryptoKeyState { NoState = 0, InProgress, KeyOk, NoKey, NoTrusted, }; /// Ask for confirmation if the message was changed. [[nodiscard]] bool queryClose() override; void annotateRecipientEditorLineWithCryptoInfo(RecipientLineNG *line); void setupActions(); void setupStatusBar(QWidget *w); void htmlToolBarVisibilityChanged(bool visible); void changeCryptoAction(); void runKeyResolver(); int validateLineWrapWidth() const; void disableWordWrap(); void checkOwnKeyExpiry(const KIdentityManagementCore::Identity &ident); std::unique_ptr fillKeyResolver(); /// Returns true if the message was modified by the user. [[nodiscard]] bool isModified() const; /// Returns true if the composer content was modified by the user. [[nodiscard]] bool isComposerModified() const; /** * Returns true if the message will be signed. */ [[nodiscard]] bool sign() const; Kleo::CryptoMessageFormat cryptoMessageFormat() const; KIdentityManagementCore::Identity mIdentity; QString mFrom; QWidget *const mMainWidget; // splitter between the headers area and the actual editor MessageComposer::ComposerViewBase *const mComposerBase; QSplitter *const mHeadersToEditorSplitter; QWidget *const mHeadersArea; QGridLayout *const mGrid; QLabel *const mLblFrom; QPushButton *const mButtonFrom; RecipientsEditor *const mRecipientEditor; QLabel *const mLblSubject; QLineEdit *const mEdtSubject; MessageComposer::RichTextComposerNg *const mRichTextComposer; TextCustomEditor::RichTextEditorWidget *const mRichTextEditorWidget; NearExpiryWarning *const mNearExpiryWarning; KMComposerGlobalAction *const mGlobalAction; QLabel *mEdtFrom = nullptr; bool mForceDisableHtml = false; bool mLastIdentityHasSigningKey = false; bool mLastIdentityHasEncryptionKey = false; QAction *mEncryptAction = nullptr; QAction *mSignAction = nullptr; QAction *mAppendSignature = nullptr; QAction *mPrependSignature = nullptr; QAction *mInsertSignatureAtCursorPosition = nullptr; QAction *mSelectAll = nullptr; QAction *mFindText = nullptr; QAction *mFindNextText = nullptr; QAction *mReplaceText = nullptr; QLabel *mStatusbarLabel = nullptr; QLabel *mCursorLineLabel = nullptr; QLabel *mCursorColumnLabel = nullptr; KToggleAction *mMarkupAction = nullptr; std::shared_ptr mExpiryChecker; bool mIsModified = false; bool mAcceptedSolution = false; KRecentFilesAction *mRecentAction = nullptr; KToggleAction *mWordWrapAction = nullptr; KToggleAction *mAutoSpellCheckingAction = nullptr; bool mLastSignActionState = false; std::shared_ptr mKeyCache; bool mLastEncryptActionState = false; QTimer *mRunKeyResolverTimer = nullptr; }; diff --git a/client/editor/composerwindowfactory.h b/client/editor/composerwindowfactory.h index db95aef..741b92e 100644 --- a/client/editor/composerwindowfactory.h +++ b/client/editor/composerwindowfactory.h @@ -1,35 +1,35 @@ // SPDX-FileCopyrightText: 2023 g10 code Gmbh // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include class QWidget; class ComposerWindow; /// Factory to create ComposerWindow. class ComposerWindowFactory { public: /// Get factory singleton. static ComposerWindowFactory &self(); /// Create a new composer dialog. /// /// This might reuse an existing and unused composer dialog instead of creating a /// new one. ComposerWindow *create(const QString &fromAddress, const QString &name, const QByteArray &bearerToken); /// Clear composer dialog metadata when not needed anymore. /// /// Ensure that there is always at least one QMainWindow active at the same time /// so that the QApplication is not deleted. void clear(ComposerWindow *composerWindow); private: ComposerWindowFactory(); - ComposerWindow * inactiveWindow = nullptr; + ComposerWindow *inactiveWindow = nullptr; }; diff --git a/client/editor/cryptostateindicatorwidget.cpp b/client/editor/cryptostateindicatorwidget.cpp index 28439a1..7691263 100644 --- a/client/editor/cryptostateindicatorwidget.cpp +++ b/client/editor/cryptostateindicatorwidget.cpp @@ -1,72 +1,71 @@ /* SPDX-FileCopyrightText: 2014-2023 Laurent Montel SPDX-License-Identifier: GPL-2.0-or-later */ #include "cryptostateindicatorwidget.h" #include #include +#include #include #include -#include static bool isLightTheme() { return qApp->palette().color(QPalette::Window).value() >= 128; } - CryptoStateIndicatorWidget::CryptoStateIndicatorWidget(QWidget *parent) : QWidget(parent) , mSignatureStateIndicator(new QLabel(this)) , mEncryptionStateIndicator(new QLabel(this)) { KColorScheme scheme(QPalette::Active, KColorScheme::View); auto hbox = new QHBoxLayout(this); hbox->setContentsMargins({}); hbox->setSpacing(0); mSignatureStateIndicator->setAlignment(Qt::AlignHCenter); mSignatureStateIndicator->setTextFormat(Qt::PlainText); mSignatureStateIndicator->setContentsMargins(6, 6, 6, 6); hbox->addWidget(mSignatureStateIndicator); mSignatureStateIndicator->setObjectName(QStringLiteral("signatureindicator")); QPalette p(mSignatureStateIndicator->palette()); p.setColor(QPalette::Window, scheme.background(KColorScheme::PositiveBackground).color()); p.setColor(QPalette::Text, scheme.foreground(KColorScheme::PositiveText).color()); mSignatureStateIndicator->setPalette(p); mSignatureStateIndicator->setAutoFillBackground(true); mEncryptionStateIndicator->setAlignment(Qt::AlignHCenter); mEncryptionStateIndicator->setTextFormat(Qt::PlainText); mEncryptionStateIndicator->setContentsMargins(6, 6, 6, 6); hbox->addWidget(mEncryptionStateIndicator); p = mEncryptionStateIndicator->palette(); if (isLightTheme()) { p.setColor(QPalette::Window, QColor(0x00, 0x80, 0xFF).lighter(180)); p.setColor(QPalette::Text, QColor(0x00, 0x80, 0xFF).darker(200)); } else { p.setColor(QPalette::Window, QColor(0x00, 0x80, 0xFF).darker(300)); p.setColor(QPalette::Text, QColor(0x00, 0x80, 0xFF).lighter(170)); } mEncryptionStateIndicator->setPalette(p); mEncryptionStateIndicator->setAutoFillBackground(true); mEncryptionStateIndicator->setObjectName(QStringLiteral("encryptionindicator")); } CryptoStateIndicatorWidget::~CryptoStateIndicatorWidget() = default; void CryptoStateIndicatorWidget::updateSignatureAndEncrypionStateIndicators(bool isSign, bool isEncrypted) { mIsEncrypted = isEncrypted; mIsSign = isSign; mSignatureStateIndicator->setText(isSign ? i18n("Message will be signed") : i18n("Message will not be signed")); mEncryptionStateIndicator->setText(isEncrypted ? i18n("Message will be encrypted") : i18n("Message will not be encrypted")); } #include "moc_cryptostateindicatorwidget.cpp" diff --git a/client/editor/job/attachmentjob.cpp b/client/editor/job/attachmentjob.cpp index bb34d45..09f5a68 100644 --- a/client/editor/job/attachmentjob.cpp +++ b/client/editor/job/attachmentjob.cpp @@ -1,131 +1,131 @@ /* SPDX-FileCopyrightText: 2009 Constantin Berzan Parts based on KMail code by: Various authors. SPDX-License-Identifier: LGPL-2.0-or-later */ #include "attachmentjob.h" -#include "contentjobbase_p.h" -#include "singlepartjob.h" #include "../part/globalpart.h" #include "../util.h" +#include "contentjobbase_p.h" +#include "singlepartjob.h" #include "editor_debug.h" using namespace MessageComposer; using namespace MessageCore; class MessageComposer::AttachmentJobPrivate : public ContentJobBasePrivate { public: AttachmentJobPrivate(AttachmentJob *qq) : ContentJobBasePrivate(qq) { } AttachmentPart::Ptr part; Q_DECLARE_PUBLIC(AttachmentJob) }; AttachmentJob::AttachmentJob(AttachmentPart::Ptr part, QObject *parent) : ContentJobBase(*new AttachmentJobPrivate(this), parent) { Q_D(AttachmentJob); d->part = part; } AttachmentJob::~AttachmentJob() = default; AttachmentPart::Ptr AttachmentJob::attachmentPart() const { Q_D(const AttachmentJob); return d->part; } void AttachmentJob::setAttachmentPart(const AttachmentPart::Ptr &part) { Q_D(AttachmentJob); d->part = part; } void AttachmentJob::doStart() { Q_D(AttachmentJob); Q_ASSERT(d->part); if (d->part->mimeType() == "multipart/digest" || d->part->mimeType() == "message/rfc822") { // this is actually a digest, so we don't want any additional headers // the attachment is really a complete multipart/digest subtype // and us adding our own headers would break it. so copy over the content // and leave it alone auto part = new KMime::Content; part->setContent(d->part->data()); part->parse(); d->subjobContents << part; process(); return; } // Set up a subjob to generate the attachment content. auto sjob = new SinglepartJob(this); sjob->setData(d->part->data()); // Figure out a charset to encode parts of the headers with. const QString dataToEncode = d->part->name() + d->part->description() + d->part->fileName(); const QByteArray charset = MessageComposer::Util::selectCharset(globalPart()->charsets(true), dataToEncode); // Set up the headers. // rfc822 forwarded messages have 7bit CTE, the message itself will have // its own CTE for the content if (d->part->mimeType() == "message/rfc822") { sjob->contentTransferEncoding()->setEncoding(KMime::Headers::CE7Bit); } else { sjob->contentTransferEncoding()->setEncoding(d->part->encoding()); } auto ct = sjob->contentType(); ct->setMimeType(d->part->mimeType()); // setMimeType() clears all other params. ct->setName(d->part->name(), charset); if (ct->isText()) { // If it is a text file, detect its charset. // sjob->contentType()->setCharset( d->detectCharset( d->part->data() ) ); // From my few tests, this is *very* unreliable. // Therefore, if we do not know which charset to use, just use UTF-8. // (cberzan) QByteArray textCharset = d->part->charset(); if (textCharset.isEmpty()) { qCWarning(EDITOR_LOG) << "No charset specified. Using UTF-8."; textCharset = "utf-8"; } ct->setCharset(textCharset); } sjob->contentDescription()->fromUnicodeString(d->part->description(), charset); auto contentDisposition = sjob->contentDisposition(); contentDisposition->setFilename(d->part->fileName()); contentDisposition->setRFC2047Charset(charset); if (d->part->isInline()) { contentDisposition->setDisposition(KMime::Headers::CDinline); } else { contentDisposition->setDisposition(KMime::Headers::CDattachment); } ContentJobBase::doStart(); } void AttachmentJob::process() { Q_D(AttachmentJob); // The content has been created by our subjob. Q_ASSERT(d->subjobContents.count() == 1); d->resultContent = d->subjobContents.constFirst(); emitResult(); } #include "moc_attachmentjob.cpp" diff --git a/client/editor/job/attachmentjob.h b/client/editor/job/attachmentjob.h index 0e84e9e..5d62fa4 100644 --- a/client/editor/job/attachmentjob.h +++ b/client/editor/job/attachmentjob.h @@ -1,37 +1,37 @@ /* SPDX-FileCopyrightText: 2009 Constantin Berzan SPDX-License-Identifier: LGPL-2.0-or-later */ #pragma once -#include "contentjobbase.h" #include "../attachment/attachmentpart.h" +#include "contentjobbase.h" namespace MessageComposer { class AttachmentJobPrivate; /** * @brief The AttachmentJob class */ class AttachmentJob : public ContentJobBase { Q_OBJECT public: explicit AttachmentJob(MessageCore::AttachmentPart::Ptr part, QObject *parent = nullptr); ~AttachmentJob() override; [[nodiscard]] MessageCore::AttachmentPart::Ptr attachmentPart() const; void setAttachmentPart(const MessageCore::AttachmentPart::Ptr &part); protected Q_SLOTS: void doStart() override; void process() override; private: Q_DECLARE_PRIVATE(AttachmentJob) }; } diff --git a/client/editor/job/autocryptheadersjob.cpp b/client/editor/job/autocryptheadersjob.cpp index f3b1066..1f9fe56 100644 --- a/client/editor/job/autocryptheadersjob.cpp +++ b/client/editor/job/autocryptheadersjob.cpp @@ -1,284 +1,284 @@ /* SPDX-FileCopyrightText: 2020 Sandro Knauß SPDX-License-Identifier: LGPL-2.0-or-later */ #include "autocryptheadersjob.h" #include "contentjobbase_p.h" -#include "singlepartjob.h" #include "../util_p.h" +#include "singlepartjob.h" #include "editor_debug.h" #include #include #include #include #include #include #include #include #include using namespace MessageComposer; class MessageComposer::AutocryptHeadersJobPrivate : public ContentJobBasePrivate { public: AutocryptHeadersJobPrivate(AutocryptHeadersJob *qq) : ContentJobBasePrivate(qq) { } ~AutocryptHeadersJobPrivate() override { // clean up in case of cancelled job for (const auto &[key, header] : gossipHeaders) { delete header; } gossipHeaders.clear(); } void emitGpgError(const GpgME::Error &error); void emitNotFoundError(const QByteArray &addr, const QByteArray &fingerprint); void fillHeaderData(KMime::Headers::Generic *header, const QByteArray &addr, bool preferEncrypted, const QByteArray &keydata); void finishOnLastSubJob(); KMime::Content *content = nullptr; KMime::Message *skeletonMessage = nullptr; // used to ensure consistent order based on key order, not random one by async subjobs delivering std::map gossipHeaders; bool preferEncrypted = false; int subJobs = 0; QString gnupgHome; GpgME::Key recipientKey; std::vector gossipKeys; Q_DECLARE_PUBLIC(AutocryptHeadersJob) }; void AutocryptHeadersJobPrivate::finishOnLastSubJob() { Q_Q(AutocryptHeadersJob); if (subJobs > 0) { return; } for (const auto &[key, header] : gossipHeaders) { content->appendHeader(header); } gossipHeaders.clear(); resultContent = content; q->emitResult(); } void AutocryptHeadersJobPrivate::emitGpgError(const GpgME::Error &error) { Q_Q(AutocryptHeadersJob); Q_ASSERT(error); const QString msg = i18n( "

An error occurred while trying to export " "the key from the backend:

" "

%1

", QString::fromLocal8Bit(error.asString())); q->setError(KJob::UserDefinedError); q->setErrorText(msg); q->emitResult(); } void AutocryptHeadersJobPrivate::emitNotFoundError(const QByteArray &addr, const QByteArray &fingerprint) { Q_Q(AutocryptHeadersJob); const QString msg = i18n( "

An error occurred while trying to export " "the key from the backend:

" "

No valid key found for user %1 (%2)

", QString::fromLatin1(addr), QString::fromLatin1(fingerprint)); q->setError(KJob::UserDefinedError); q->setErrorText(msg); q->emitResult(); } void AutocryptHeadersJobPrivate::fillHeaderData(KMime::Headers::Generic *header, const QByteArray &addr, bool preferEncrypted, const QByteArray &keydata) { QByteArray parameters = "addr=" + addr + "; "; if (preferEncrypted) { parameters += "prefer-encrypt=mutual; "; } parameters += "keydata=\n "; auto encoded = KCodecs::base64Encode(keydata).replace('\n', QByteArray()); const auto length = encoded.size(); const auto lineLength = 76; auto start = 0; auto column = 1; while (start < length) { const auto midLength = std::min(length - start, lineLength - column); parameters += encoded.mid(start, midLength); start += midLength; column += midLength; if (column >= lineLength) { parameters += "\n "; column = 1; } } header->from7BitString(parameters); } AutocryptHeadersJob::AutocryptHeadersJob(QObject *parent) : ContentJobBase(*new AutocryptHeadersJobPrivate(this), parent) { } AutocryptHeadersJob::~AutocryptHeadersJob() = default; void AutocryptHeadersJob::setContent(KMime::Content *content) { Q_D(AutocryptHeadersJob); d->content = content; if (content) { d->content->assemble(); } } void AutocryptHeadersJob::setSkeletonMessage(KMime::Message *skeletonMessage) { Q_D(AutocryptHeadersJob); d->skeletonMessage = skeletonMessage; } void AutocryptHeadersJob::setGnupgHome(const QString &path) { Q_D(AutocryptHeadersJob); d->gnupgHome = path; } void AutocryptHeadersJob::setSenderKey(const GpgME::Key &key) { Q_D(AutocryptHeadersJob); d->recipientKey = key; } void AutocryptHeadersJob::setPreferEncrypted(bool preferEncrypted) { Q_D(AutocryptHeadersJob); d->preferEncrypted = preferEncrypted; } void AutocryptHeadersJob::setGossipKeys(const std::vector &gossipKeys) { Q_D(AutocryptHeadersJob); d->gossipKeys = gossipKeys; } void AutocryptHeadersJob::process() { Q_D(AutocryptHeadersJob); 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) { Q_ASSERT(d->subjobContents.size() == 1); d->content = d->subjobContents.constFirst(); } auto job = QGpgME::openpgp()->publicKeyExportJob(false); Q_ASSERT(job); if (!d->gnupgHome.isEmpty()) { QGpgME::Job::context(job)->setEngineHomeDirectory(d->gnupgHome.toUtf8().constData()); } if (!d->recipientKey.isNull() && !d->recipientKey.isInvalid()) { connect(job, &QGpgME::ExportJob::result, this, [this, d](const GpgME::Error &error, const QByteArray &keydata) { d->subJobs--; if (AutocryptHeadersJob::error()) { // When the job already has failed do nothing. return; } if (error) { d->emitGpgError(error); return; } if (keydata.isEmpty()) { d->emitNotFoundError(d->skeletonMessage->from()->addresses()[0], d->recipientKey.primaryFingerprint()); return; } auto autocrypt = new KMime::Headers::Generic("Autocrypt"); d->fillHeaderData(autocrypt, d->skeletonMessage->from()->addresses()[0], d->preferEncrypted, keydata); d->skeletonMessage->setHeader(autocrypt); d->skeletonMessage->assemble(); d->finishOnLastSubJob(); }); d->subJobs++; job->start(QStringList(QString::fromLatin1(d->recipientKey.primaryFingerprint()))); job->setExportFlags(GpgME::Context::ExportMinimal); } const auto keys = d->gossipKeys; for (const auto &key : keys) { if (QByteArray(key.primaryFingerprint()) == QByteArray(d->recipientKey.primaryFingerprint())) { continue; } auto gossipJob = QGpgME::openpgp()->publicKeyExportJob(false); Q_ASSERT(gossipJob); if (!d->gnupgHome.isEmpty()) { QGpgME::Job::context(gossipJob)->setEngineHomeDirectory(d->gnupgHome.toUtf8().constData()); } connect(gossipJob, &QGpgME::ExportJob::result, this, [this, d, key](const GpgME::Error &error, const QByteArray &keydata) { d->subJobs--; if (AutocryptHeadersJob::error()) { // When the job already has failed do nothing. return; } if (error) { d->emitGpgError(error); return; } if (keydata.isEmpty()) { d->emitNotFoundError(key.userID(0).email(), key.primaryFingerprint()); return; } auto header = new KMime::Headers::Generic("Autocrypt-Gossip"); d->fillHeaderData(header, key.userID(0).email(), false, keydata); d->gossipHeaders.insert({QByteArray(key.primaryFingerprint()), header}); d->finishOnLastSubJob(); }); d->subJobs++; gossipJob->start(QStringList(QString::fromLatin1(key.primaryFingerprint()))); gossipJob->setExportFlags(GpgME::Context::ExportMinimal); } if (d->subJobs == 0) { d->resultContent = d->content; emitResult(); } } #include "moc_autocryptheadersjob.cpp" diff --git a/client/editor/job/encryptjob.cpp b/client/editor/job/encryptjob.cpp index f4f187e..138eacb 100644 --- a/client/editor/job/encryptjob.cpp +++ b/client/editor/job/encryptjob.cpp @@ -1,279 +1,279 @@ /* SPDX-FileCopyrightText: 2009 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.net SPDX-FileCopyrightText: 2009 Leo Franchi SPDX-License-Identifier: LGPL-2.0-or-later */ #include "encryptjob.h" +#include "../part/infopart.h" +#include "../util_p.h" #include "contentjobbase_p.h" #include "protectedheadersjob.h" -#include "../util_p.h" -#include "../part/infopart.h" #include #include #include "editor_debug.h" #include #include #include #include using namespace MessageComposer; class MessageComposer::EncryptJobPrivate : public ContentJobBasePrivate { public: EncryptJobPrivate(EncryptJob *qq) : ContentJobBasePrivate(qq) { } QString gnupgHome; 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() = default; 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; } void EncryptJob::setGnupgHome(const QString &path) { Q_D(EncryptJob); d->gnupgHome = path; } 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(EDITOR_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) { auto 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(); }); if (!appendSubjob(pJob)) { qCWarning(EDITOR_LOG) << "Impossible to add subjob"; } } ContentJobBase::doStart(); } void EncryptJob::slotResult(KJob *job) { // Q_D(EncryptJob); if (error() || job->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 { qCWarning(EDITOR_LOG) << "HELP! Encrypt job but have protocol to encrypt with."; return; } Q_ASSERT(proto); // for now just do the main recipients QByteArray content; d->content->assemble(); if (d->format & Kleo::InlineOpenPGPFormat) { content = d->content->body(); } else { content = d->content->encodedContent(); } qCDebug(EDITOR_LOG) << "got backend, starting job"; QGpgME::EncryptJob *eJob = proto->encryptJob(!d->binaryHint(d->format), d->format == Kleo::InlineOpenPGPFormat); if (!(d->format & Kleo::InlineOpenPGPFormat)) { eJob->setInputEncoding(GpgME::Data::MimeEncoding); } if (!d->gnupgHome.isEmpty()) { QGpgME::Job::context(eJob)->setEngineHomeDirectory(d->gnupgHome.toUtf8().constData()); } QObject::connect( eJob, &QGpgME::EncryptJob::result, this, [this, d](const GpgME::EncryptionResult &result, const QByteArray &cipherText, const QString &auditLogAsHtml, const GpgME::Error &auditLogError) { Q_UNUSED(auditLogAsHtml) Q_UNUSED(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(); }); const auto error = eJob->start(d->keys, content, true); if (error.code()) { eJob->deleteLater(); setError(error.code()); setErrorText(QString::fromLocal8Bit(error.asString())); emitResult(); } } #include "moc_encryptjob.cpp" diff --git a/client/editor/job/inserttextfilejob.cpp b/client/editor/job/inserttextfilejob.cpp index 9f55602..4c7adaa 100644 --- a/client/editor/job/inserttextfilejob.cpp +++ b/client/editor/job/inserttextfilejob.cpp @@ -1,82 +1,82 @@ /* * SPDX-FileCopyrightText: 2010 Thomas McGuire * * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "inserttextfilejob.h" #include #include "editor_debug.h" -#include -#include #include #include +#include +#include using namespace MessageComposer; class MessageComposer::InsertTextFileJobPrivate { public: InsertTextFileJobPrivate(QTextEdit *editor, const QUrl &url) : mEditor(editor) , mUrl(url) { } QPointer mEditor; QUrl mUrl; QString mEncoding; QByteArray mFileData; }; InsertTextFileJob::InsertTextFileJob(QTextEdit *editor, const QUrl &url) : KJob(editor) , d(new MessageComposer::InsertTextFileJobPrivate(editor, url)) { } InsertTextFileJob::~InsertTextFileJob() = default; void InsertTextFileJob::slotGetJobFinished(QNetworkReply *reply) { if (reply->error() != QNetworkReply::NoError) { qCWarning(EDITOR_LOG) << reply->errorString(); setError(reply->error()); setErrorText(reply->errorString()); emitResult(); return; } if (d->mEditor) { if (!d->mEncoding.isEmpty()) { QStringDecoder fileCodec(d->mEncoding.toLatin1().constData()); if (fileCodec.isValid()) { d->mEditor->textCursor().insertText(fileCodec.decode(d->mFileData.data())); } else { d->mEditor->textCursor().insertText(QString::fromLocal8Bit(d->mFileData.data())); } } else { d->mEditor->textCursor().insertText(QString::fromLocal8Bit(d->mFileData.data())); } } emitResult(); } void InsertTextFileJob::setEncoding(const QString &encoding) { d->mEncoding = encoding; } void InsertTextFileJob::start() { auto qnam = new QNetworkAccessManager(this); QNetworkRequest request(d->mUrl); auto reply = qnam->get(request); connect(reply, &QNetworkReply::finished, this, [this, reply]() { slotGetJobFinished(reply); }); } #include "moc_inserttextfilejob.cpp" diff --git a/client/editor/job/inserttextfilejob.h b/client/editor/job/inserttextfilejob.h index 4ee4a4d..97abd16 100644 --- a/client/editor/job/inserttextfilejob.h +++ b/client/editor/job/inserttextfilejob.h @@ -1,42 +1,41 @@ /* * SPDX-FileCopyrightText: 2010 Thomas McGuire * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once - #include #include class QTextEdit; class QNetworkReply; namespace KIO { class Job; } namespace MessageComposer { class InsertTextFileJobPrivate; /** * A job that downloads a given URL, interprets the result as a text file with the * given encoding and then inserts the text into the editor. */ class InsertTextFileJob : public KJob { Q_OBJECT public: explicit InsertTextFileJob(QTextEdit *editor, const QUrl &url); ~InsertTextFileJob() override; void setEncoding(const QString &encoding); void start() override; private: void slotGetJobFinished(QNetworkReply *reply); std::unique_ptr const d; }; } diff --git a/client/editor/job/itipjob.cpp b/client/editor/job/itipjob.cpp index e663f6a..e4c216c 100644 --- a/client/editor/job/itipjob.cpp +++ b/client/editor/job/itipjob.cpp @@ -1,139 +1,139 @@ /* SPDX-FileCopyrightText: 2023 Daniel Vrátil SPDX-License-Identifier: LGPL-2.0-or-later */ #include "itipjob.h" +#include "../part/itippart.h" #include "contentjobbase_p.h" #include "multipartjob.h" #include "singlepartjob.h" -#include "../part/itippart.h" #include #include #include using namespace MessageComposer; class MessageComposer::ItipJobPrivate : public ContentJobBasePrivate { public: ItipJobPrivate(ItipJob *qq) : ContentJobBasePrivate(qq) { } ContentJobBase *createStandardItipJob(); SinglepartJob *createOutlookItipJob(); SinglepartJob *createInvitationBodyJob(); SinglepartJob *createInvitationJob(); ItipPart *itipPart = nullptr; Q_DECLARE_PUBLIC(ItipJob) }; SinglepartJob *ItipJobPrivate::createInvitationBodyJob() { auto job = new SinglepartJob; // No parent. job->contentType()->setMimeType("text/plain"); job->contentType()->setCharset("utf-8"); job->contentType()->setParameter(QStringLiteral("method"), itipPart->method()); job->contentTransferEncoding()->setEncoding(KMime::Headers::CEquPr); job->contentDisposition()->setDisposition(KMime::Headers::CDinline); job->setData(KMime::CRLFtoLF(itipPart->invitationBody().toUtf8())); return job; } SinglepartJob *ItipJobPrivate::createInvitationJob() { auto job = new SinglepartJob; job->contentType()->setMimeType("text/calendar"); job->contentType()->setCharset("utf-8"); job->contentType()->setName(QStringLiteral("cal.ics"), "utf-8"); job->contentType()->setParameter(QStringLiteral("method"), itipPart->method()); job->contentDisposition()->setDisposition(KMime::Headers::CDattachment); job->contentTransferEncoding()->setEncoding(KMime::Headers::CEquPr); job->setData(KMime::CRLFtoLF(itipPart->invitation().toUtf8())); return job; } ContentJobBase *ItipJobPrivate::createStandardItipJob() { auto bodyJob = createInvitationBodyJob(); if (itipPart->invitation().isEmpty()) { return bodyJob; } auto mpJob = new MultipartJob; mpJob->setMultipartSubtype("mixed"); mpJob->appendSubjob(bodyJob); mpJob->appendSubjob(createInvitationJob()); return mpJob; } SinglepartJob *ItipJobPrivate::createOutlookItipJob() { auto job = new SinglepartJob; job->contentType()->setMimeType("text/calendar"); job->contentType()->setName(QStringLiteral("cal.ics"), "utf-8"); job->contentType()->setParameter(QStringLiteral("method"), QStringLiteral("request")); job->contentType()->setCharset("utf-8"); if (!itipPart->invitation().isEmpty()) { job->contentDisposition()->setDisposition(KMime::Headers::CDinline); job->contentTransferEncoding()->setEncoding(KMime::Headers::CEquPr); job->setData(KMime::CRLFtoLF(itipPart->invitation().toUtf8())); } return job; } ItipJob::ItipJob(ItipPart *itipPart, QObject *parent) : ContentJobBase(*new ItipJobPrivate(this), parent) { Q_D(ItipJob); d->itipPart = itipPart; } ItipJob::~ItipJob() = default; ItipPart *ItipJob::itipPart() const { Q_D(const ItipJob); return d->itipPart; } void ItipJob::setItipPart(ItipPart *part) { Q_D(ItipJob); d->itipPart = part; } void ItipJob::doStart() { Q_D(ItipJob); Q_ASSERT(d->itipPart); if (d->itipPart->outlookConformInvitation()) { appendSubjob(d->createOutlookItipJob()); } else { appendSubjob(d->createStandardItipJob()); } ContentJobBase::doStart(); } void ItipJob::process() { Q_D(ItipJob); // The content has been created by our subjob. Q_ASSERT(d->subjobContents.count() == 1); d->resultContent = d->subjobContents.constFirst(); emitResult(); } #include "moc_itipjob.cpp" diff --git a/client/editor/job/jobbase.h b/client/editor/job/jobbase.h index 46d317c..3b325e7 100644 --- a/client/editor/job/jobbase.h +++ b/client/editor/job/jobbase.h @@ -1,50 +1,49 @@ /* SPDX-FileCopyrightText: 2009 Constantin Berzan SPDX-License-Identifier: LGPL-2.0-or-later */ #pragma once - #include #include namespace MessageComposer { class GlobalPart; class JobBasePrivate; /** A dummy abstract class defining some errors pertaining to the Composer. It is meant to be subclassed. */ class JobBase : public KCompositeJob { Q_OBJECT public: using List = QList; enum Error { BugError = UserDefinedError + 1, IncompleteError, UserCancelledError, UserError = UserDefinedError + 42, }; explicit JobBase(QObject *parent = nullptr); ~JobBase() override; // asserts if no Composer parent GlobalPart *globalPart(); protected: JobBasePrivate *const d_ptr; JobBase(JobBasePrivate &dd, QObject *parent); private: Q_DECLARE_PRIVATE(JobBase) }; } // namespace MessageComposer diff --git a/client/editor/job/maintextjob.cpp b/client/editor/job/maintextjob.cpp index 5103e85..f8fd319 100644 --- a/client/editor/job/maintextjob.cpp +++ b/client/editor/job/maintextjob.cpp @@ -1,274 +1,274 @@ /* SPDX-FileCopyrightText: 2009 Constantin Berzan SPDX-License-Identifier: LGPL-2.0-or-later */ #include "maintextjob.h" -#include "contentjobbase_p.h" -#include "multipartjob.h" -#include "singlepartjob.h" #include "../part/globalpart.h" #include "../part/textpart.h" #include "../util.h" +#include "contentjobbase_p.h" +#include "multipartjob.h" +#include "singlepartjob.h" #include #include "editor_debug.h" #include #include #include using namespace MessageComposer; class MessageComposer::MainTextJobPrivate : public ContentJobBasePrivate { public: MainTextJobPrivate(MainTextJob *qq) : ContentJobBasePrivate(qq) { } bool chooseSourcePlainText(); bool chooseCharsetAndEncode(); bool chooseCharset(); bool encodeTexts(); SinglepartJob *createPlainTextJob(); SinglepartJob *createHtmlJob(); SinglepartJob *createImageJob(const QSharedPointer &image); TextPart *textPart = nullptr; QByteArray chosenCharset; QString sourcePlainText; QByteArray encodedPlainText; QByteArray encodedHtml; Q_DECLARE_PUBLIC(MainTextJob) }; bool MainTextJobPrivate::chooseSourcePlainText() { Q_Q(MainTextJob); Q_ASSERT(textPart); if (textPart->isWordWrappingEnabled()) { sourcePlainText = textPart->wrappedPlainText(); if (sourcePlainText.isEmpty() && !textPart->cleanPlainText().isEmpty()) { q->setError(JobBase::BugError); q->setErrorText(i18n("Asked to use word wrapping, but not given wrapped plain text.")); return false; } } else { sourcePlainText = textPart->cleanPlainText(); if (sourcePlainText.isEmpty() && !textPart->wrappedPlainText().isEmpty()) { q->setError(JobBase::BugError); q->setErrorText(i18n("Asked not to use word wrapping, but not given clean plain text.")); return false; } } return true; } bool MainTextJobPrivate::chooseCharsetAndEncode() { Q_Q(MainTextJob); const QList charsets = q->globalPart()->charsets(true); if (charsets.isEmpty()) { q->setError(JobBase::BugError); q->setErrorText( i18n("No charsets were available for encoding. Please check your configuration and make sure it contains at least one charset for sending.")); return false; } Q_ASSERT(textPart); QString toTry = sourcePlainText; if (textPart->isHtmlUsed()) { toTry = textPart->cleanHtml(); } chosenCharset = MessageComposer::Util::selectCharset(charsets, toTry); if (!chosenCharset.isEmpty()) { // Good, found a charset that encodes the data without loss. return encodeTexts(); } else { // No good charset was found. if (q->globalPart()->isGuiEnabled() && textPart->warnBadCharset()) { // Warn the user and give them a chance to go back. int result = KMessageBox::warningTwoActions(q->globalPart()->parentWidgetForGui(), i18n("Encoding the message with %1 will lose some characters.\n" "Do you want to continue?", QString::fromLatin1(charsets.first())), i18nc("@title:window", "Some Characters Will Be Lost"), KGuiItem(i18n("Lose Characters")), KGuiItem(i18n("Change Encoding"))); if (result == KMessageBox::ButtonCode::SecondaryAction) { q->setError(JobBase::UserCancelledError); q->setErrorText(i18n("User decided to change the encoding.")); return false; } else { chosenCharset = charsets.first(); return encodeTexts(); } } else if (textPart->warnBadCharset()) { // Should warn user but no Gui available. qCDebug(EDITOR_LOG) << "warnBadCharset but Gui is disabled."; q->setError(JobBase::UserError); q->setErrorText(i18n("The selected encoding (%1) cannot fully encode the message.", QString::fromLatin1(charsets.first()))); return false; } else { // OK to go ahead with a bad charset. chosenCharset = charsets.first(); return encodeTexts(); // FIXME: This is based on the assumption that QTextCodec will replace // unknown characters with '?' or some other meaningful thing. The code in // QTextCodec indeed uses '?', but this behaviour is not documented. } } // Should not reach here. Q_ASSERT(false); return false; } bool MainTextJobPrivate::encodeTexts() { Q_Q(MainTextJob); QStringEncoder codec(chosenCharset.constData()); if (!codec.isValid()) { qCCritical(EDITOR_LOG) << "Could not get text codec for charset" << chosenCharset; q->setError(JobBase::BugError); q->setErrorText(i18n("Could not get text codec for charset \"%1\".", QString::fromLatin1(chosenCharset))); return false; } encodedPlainText = codec.encode(sourcePlainText); if (!textPart->cleanHtml().isEmpty()) { encodedHtml = codec.encode(textPart->cleanHtml()); } qCDebug(EDITOR_LOG) << "Done."; return true; } SinglepartJob *MainTextJobPrivate::createPlainTextJob() { auto cjob = new SinglepartJob; // No parent. cjob->contentType()->setMimeType("text/plain"); cjob->contentType()->setCharset(chosenCharset); cjob->setData(encodedPlainText); // TODO standard recommends Content-ID. return cjob; } SinglepartJob *MainTextJobPrivate::createHtmlJob() { auto cjob = new SinglepartJob; // No parent. cjob->contentType()->setMimeType("text/html"); cjob->contentType()->setCharset(chosenCharset); const QByteArray data = KPIMTextEdit::RichTextComposerImages::imageNamesToContentIds(encodedHtml, textPart->embeddedImages()); cjob->setData(data); // TODO standard recommends Content-ID. return cjob; } SinglepartJob *MainTextJobPrivate::createImageJob(const QSharedPointer &image) { Q_Q(MainTextJob); // The image is a PNG encoded with base64. auto cjob = new SinglepartJob; // No parent. cjob->contentType()->setMimeType("image/png"); const QByteArray charset = MessageComposer::Util::selectCharset(q->globalPart()->charsets(true), image->imageName); Q_ASSERT(!charset.isEmpty()); cjob->contentType()->setName(image->imageName, charset); cjob->contentTransferEncoding()->setEncoding(KMime::Headers::CEbase64); cjob->contentTransferEncoding()->setDecoded(false); // It is already encoded. cjob->contentID()->setIdentifier(image->contentID.toLatin1()); qCDebug(EDITOR_LOG) << "cid" << cjob->contentID()->identifier(); cjob->setData(image->image); return cjob; } MainTextJob::MainTextJob(TextPart *textPart, QObject *parent) : ContentJobBase(*new MainTextJobPrivate(this), parent) { Q_D(MainTextJob); d->textPart = textPart; } MainTextJob::~MainTextJob() = default; TextPart *MainTextJob::textPart() const { Q_D(const MainTextJob); return d->textPart; } void MainTextJob::setTextPart(TextPart *part) { Q_D(MainTextJob); d->textPart = part; } void MainTextJob::doStart() { Q_D(MainTextJob); Q_ASSERT(d->textPart); // Word wrapping. if (!d->chooseSourcePlainText()) { // chooseSourcePlainText has set an error. Q_ASSERT(error()); emitResult(); return; } // Charset. if (!d->chooseCharsetAndEncode()) { // chooseCharsetAndEncode has set an error. Q_ASSERT(error()); emitResult(); return; } // Assemble the Content. SinglepartJob *plainJob = d->createPlainTextJob(); if (d->encodedHtml.isEmpty()) { qCDebug(EDITOR_LOG) << "Making text/plain"; // Content is text/plain. appendSubjob(plainJob); } else { auto alternativeJob = new MultipartJob; alternativeJob->setMultipartSubtype("alternative"); alternativeJob->appendSubjob(plainJob); // text/plain first. alternativeJob->appendSubjob(d->createHtmlJob()); // text/html second. if (!d->textPart->hasEmbeddedImages()) { qCDebug(EDITOR_LOG) << "Have no images. Making multipart/alternative."; // Content is multipart/alternative. appendSubjob(alternativeJob); } else { qCDebug(EDITOR_LOG) << "Have related images. Making multipart/related."; // Content is multipart/related with a multipart/alternative sub-Content. auto multipartJob = new MultipartJob; multipartJob->setMultipartSubtype("related"); multipartJob->appendSubjob(alternativeJob); const auto embeddedImages = d->textPart->embeddedImages(); for (const QSharedPointer &image : embeddedImages) { multipartJob->appendSubjob(d->createImageJob(image)); } appendSubjob(multipartJob); } } ContentJobBase::doStart(); } void MainTextJob::process() { Q_D(MainTextJob); // The content has been created by our subjob. Q_ASSERT(d->subjobContents.count() == 1); d->resultContent = d->subjobContents.constFirst(); emitResult(); } #include "moc_maintextjob.cpp" diff --git a/client/editor/job/multipartjob.cpp b/client/editor/job/multipartjob.cpp index 03a1780..97ff271 100644 --- a/client/editor/job/multipartjob.cpp +++ b/client/editor/job/multipartjob.cpp @@ -1,69 +1,69 @@ /* SPDX-FileCopyrightText: 2009 Constantin Berzan SPDX-License-Identifier: LGPL-2.0-or-later */ #include "multipartjob.h" #include "contentjobbase_p.h" #include "editor_debug.h" #include using namespace MessageComposer; class MessageComposer::MultipartJobPrivate : public ContentJobBasePrivate { public: MultipartJobPrivate(MultipartJob *qq) : ContentJobBasePrivate(qq) { } QByteArray subtype; }; MultipartJob::MultipartJob(QObject *parent) : ContentJobBase(*new MultipartJobPrivate(this), parent) { } MultipartJob::~MultipartJob() = default; QByteArray MultipartJob::multipartSubtype() const { Q_D(const MultipartJob); return d->subtype; } void MultipartJob::setMultipartSubtype(const QByteArray &subtype) { Q_D(MultipartJob); d->subtype = subtype; } void MultipartJob::process() { Q_D(MultipartJob); Q_ASSERT(d->resultContent == nullptr); // Not processed before. Q_ASSERT(!d->subtype.isEmpty()); d->resultContent = new KMime::Content; auto contentType = d->resultContent->contentType(true); contentType->setMimeType("multipart/" + d->subtype); contentType->setBoundary(KMime::multiPartBoundary()); d->resultContent->contentTransferEncoding()->setEncoding(KMime::Headers::CE7Bit); d->resultContent->setPreamble("This is a multi-part message in MIME format.\n"); for (KMime::Content *c : std::as_const(d->subjobContents)) { d->resultContent->appendContent(c); if (c->contentTransferEncoding()->encoding() == KMime::Headers::CE8Bit) { d->resultContent->contentTransferEncoding()->setEncoding(KMime::Headers::CE8Bit); break; } } qCDebug(EDITOR_LOG) << "Created" << d->resultContent->contentType()->mimeType() << "content with" << d->resultContent->contents().count() - << "subjobContents."; + << "subjobContents."; emitResult(); } #include "moc_multipartjob.cpp" diff --git a/client/editor/job/protectedheadersjob.cpp b/client/editor/job/protectedheadersjob.cpp index 3e2f7fc..698bd87 100644 --- a/client/editor/job/protectedheadersjob.cpp +++ b/client/editor/job/protectedheadersjob.cpp @@ -1,159 +1,159 @@ /* SPDX-FileCopyrightText: 2020 Sandro Knauß SPDX-License-Identifier: LGPL-2.0-or-later */ #include "protectedheadersjob.h" #include "contentjobbase_p.h" -#include "singlepartjob.h" #include "editor_debug.h" +#include "singlepartjob.h" #include #include using namespace MessageComposer; class MessageComposer::ProtectedHeadersJobPrivate : public ContentJobBasePrivate { public: ProtectedHeadersJobPrivate(ProtectedHeadersJob *qq) : ContentJobBasePrivate(qq) { } KMime::Content *content = nullptr; KMime::Message *skeletonMessage = nullptr; bool obvoscate = false; Q_DECLARE_PUBLIC(ProtectedHeadersJob) }; ProtectedHeadersJob::ProtectedHeadersJob(QObject *parent) : ContentJobBase(*new ProtectedHeadersJobPrivate(this), parent) { } ProtectedHeadersJob::~ProtectedHeadersJob() = default; void ProtectedHeadersJob::setContent(KMime::Content *content) { Q_D(ProtectedHeadersJob); d->content = content; if (content) { d->content->assemble(); } } void ProtectedHeadersJob::setSkeletonMessage(KMime::Message *skeletonMessage) { Q_D(ProtectedHeadersJob); d->skeletonMessage = skeletonMessage; } void ProtectedHeadersJob::setObvoscate(bool obvoscate) { Q_D(ProtectedHeadersJob); d->obvoscate = obvoscate; } void ProtectedHeadersJob::doStart() { Q_D(ProtectedHeadersJob); Q_ASSERT(d->resultContent == nullptr); // Not processed before. Q_ASSERT(d->skeletonMessage); // We need a skeletonMessage to proceed auto subject = d->skeletonMessage->header(); if (d->obvoscate && subject) { // Create protected header lagacy mimepart with replaced headers auto cjob = new SinglepartJob; auto ct = cjob->contentType(); ct->setMimeType("text/plain"); ct->setCharset(subject->rfc2047Charset()); ct->setParameter(QStringLiteral("protected-headers"), QStringLiteral("v1")); cjob->contentDisposition()->setDisposition(KMime::Headers::contentDisposition::CDinline); cjob->setData(subject->type() + QByteArray(": ") + subject->asUnicodeString().toUtf8()); QObject::connect(cjob, &SinglepartJob::finished, this, [d, cjob]() { auto mixedPart = new KMime::Content(); const QByteArray boundary = KMime::multiPartBoundary(); mixedPart->contentType()->setMimeType("multipart/mixed"); mixedPart->contentType(false)->setBoundary(boundary); mixedPart->appendContent(cjob->content()); // 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(); } mixedPart->appendContent(d->content); d->content = mixedPart; }); if (!appendSubjob(cjob)) { qCWarning(EDITOR_LOG) << "Impossible to add subjob"; } } ContentJobBase::doStart(); } void ProtectedHeadersJob::process() { Q_D(ProtectedHeadersJob); // 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(); } auto subject = d->skeletonMessage->header(); const auto headers = d->skeletonMessage->headers(); for (const auto &header : headers) { const QByteArray headerType(header->type()); if (headerType.startsWith("X-KMail-")) { continue; } if (headerType == "Bcc") { continue; } if (headerType.startsWith("Content-")) { continue; } // A workaround for #439958 // KMime strips sometimes the newlines from long headers, if those // headers are in the signature block, this breaks the signature. // The simplest workaround is not to sign those headers until this // get fixed in KMime. if (header->as7BitString().length() > 70) { continue; } auto copyHeader = KMime::Headers::createHeader(headerType); if (!copyHeader) { copyHeader = new KMime::Headers::Generic(headerType.constData(), headerType.size()); } copyHeader->from7BitString(header->as7BitString(false)); d->content->appendHeader(copyHeader); } if (d->obvoscate && subject) { subject->clear(); subject->from7BitString("..."); } auto contentType = d->content->header(); contentType->setParameter(QStringLiteral("protected-headers"), QStringLiteral("v1")); d->resultContent = d->content; emitResult(); } #include "moc_protectedheadersjob.cpp" diff --git a/client/editor/job/protectedheadersjob.h b/client/editor/job/protectedheadersjob.h index 9da2754..399c1d6 100644 --- a/client/editor/job/protectedheadersjob.h +++ b/client/editor/job/protectedheadersjob.h @@ -1,48 +1,48 @@ /* SPDX-FileCopyrightText: 2020 Sandro Knauß SPDX-License-Identifier: LGPL-2.0-or-later */ #pragma once -#include "contentjobbase.h" #include "../part/infopart.h" +#include "contentjobbase.h" namespace KMime { class Content; } namespace MessageComposer { class ProtectedHeadersJobPrivate; /** Copies headers from skeleton message to content. It is used for Protected Headers for Cryptographic E-mail currently a draft for RFC: https://datatracker.ietf.org/doc/draft-autocrypt-lamps-protected-headers/ Used as a subjob of EncryptJob/SignJob/SignEncryptJob */ class ProtectedHeadersJob : public ContentJobBase { Q_OBJECT public: explicit ProtectedHeadersJob(QObject *parent = nullptr); ~ProtectedHeadersJob() override; void setContent(KMime::Content *content); void setSkeletonMessage(KMime::Message *skeletonMessage); void setObvoscate(bool obvoscate); protected Q_SLOTS: void doStart() override; void process() override; private: Q_DECLARE_PRIVATE(ProtectedHeadersJob) }; } diff --git a/client/editor/job/signencryptjob.cpp b/client/editor/job/signencryptjob.cpp index 5dbf952..b70eda6 100644 --- a/client/editor/job/signencryptjob.cpp +++ b/client/editor/job/signencryptjob.cpp @@ -1,276 +1,276 @@ /* SPDX-FileCopyrightText: 2009 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.net SPDX-FileCopyrightText: 2009 Leo Franchi SPDX-License-Identifier: LGPL-2.0-or-later */ #include "signencryptjob.h" +#include "../util_p.h" #include "contentjobbase_p.h" #include "protectedheadersjob.h" -#include "../util_p.h" #include #include #include "editor_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() = default; 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) { auto 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(); }); if (!appendSubjob(pJob)) { qCWarning(EDITOR_LOG) << "Impossible to add subjob"; } } ContentJobBase::doStart(); } void SignEncryptJob::slotResult(KJob *job) { // Q_D(SignEncryptJob); if (error() || job->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 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.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(EDITOR_LOG) << "creating signencrypt from:" << proto->name() << proto->displayName(); 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(); } QGpgME::SignEncryptJob *job(proto->signEncryptJob(!d->binaryHint(d->format), d->format == Kleo::InlineOpenPGPFormat)); QObject::connect(job, &QGpgME::SignEncryptJob::result, this, [this, d](const GpgME::SigningResult &signingResult, const GpgME::EncryptionResult &encryptionResult, const QByteArray &cipherText, const QString &auditLogAsHtml, const GpgME::Error &auditLogError) { Q_UNUSED(auditLogAsHtml) Q_UNUSED(auditLogError) if (signingResult.error()) { qCDebug(EDITOR_LOG) << "signing failed:" << signingResult.error().asString(); setError(signingResult.error().code()); setErrorText(QString::fromLocal8Bit(signingResult.error().asString())); emitResult(); return; } if (encryptionResult.error()) { qCDebug(EDITOR_LOG) << "encrypting failed:" << encryptionResult.error().asString(); setError(encryptionResult.error().code()); setErrorText(QString::fromLocal8Bit(encryptionResult.error().asString())); emitResult(); return; } QByteArray signatureHashAlgo = signingResult.createdSignature(0).hashAlgorithmAsString(); d->resultContent = MessageComposer::Util::composeHeadersAndBody(d->content, cipherText, d->format, false, signatureHashAlgo); emitResult(); }); const auto error = job->start(d->signers, d->encKeys, content, false); if (error.code()) { job->deleteLater(); setError(error.code()); setErrorText(QString::fromLocal8Bit(error.asString())); emitResult(); } } #include "moc_signencryptjob.cpp" diff --git a/client/editor/job/signencryptjob.h b/client/editor/job/signencryptjob.h index b8be694..48aba2f 100644 --- a/client/editor/job/signencryptjob.h +++ b/client/editor/job/signencryptjob.h @@ -1,65 +1,65 @@ /* SPDX-FileCopyrightText: 2009 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.net SPDX-FileCopyrightText: 2009 Leo Franchi SPDX-License-Identifier: LGPL-2.0-or-later */ #pragma once +#include "../part/infopart.h" #include "abstractencryptjob.h" #include "contentjobbase.h" -#include "../part/infopart.h" #include #include #include namespace KMime { class Content; } namespace MessageComposer { class SignEncryptJobPrivate; /** Signs and encrypt the contents of a message. Used when doing inline pgp sign+encrypt */ class SignEncryptJob : public ContentJobBase, public MessageComposer::AbstractEncryptJob { Q_OBJECT public: explicit SignEncryptJob(QObject *parent = nullptr); ~SignEncryptJob() override; void setContent(KMime::Content *content); void setCryptoMessageFormat(Kleo::CryptoMessageFormat format); void setSigningKeys(const std::vector &signers); void setEncryptionKeys(const std::vector &keys) override; void setRecipients(const QStringList &rec) override; void setSkeletonMessage(KMime::Message *skeletonMessage); void setProtectedHeaders(bool protectedHeaders); void setProtectedHeadersObvoscate(bool protectedHeadersObvoscate); [[nodiscard]] std::vector encryptionKeys() const override; [[nodiscard]] QStringList recipients() const override; [[nodiscard]] KMime::Content *origContent(); protected Q_SLOTS: void doStart() override; void slotResult(KJob *job) override; void process() override; private: Q_DECLARE_PRIVATE(SignEncryptJob) }; } diff --git a/client/editor/job/signjob.cpp b/client/editor/job/signjob.cpp index 064a3f4..2beeb31 100644 --- a/client/editor/job/signjob.cpp +++ b/client/editor/job/signjob.cpp @@ -1,291 +1,290 @@ /* SPDX-FileCopyrightText: 2009 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.net SPDX-FileCopyrightText: 2009 Leo Franchi SPDX-License-Identifier: LGPL-2.0-or-later */ #include "signjob.h" +#include "../util_p.h" #include "contentjobbase_p.h" #include "protectedheadersjob.h" -#include "../util_p.h" #include #include #include #include "editor_debug.h" #include #include #include #include #include #include #include using namespace MessageComposer; class MessageComposer::SignJobPrivate : public ContentJobBasePrivate { public: SignJobPrivate(SignJob *qq) : ContentJobBasePrivate(qq) { } KMime::Content *content = nullptr; KMime::Message *skeletonMessage = nullptr; std::vector signers; Kleo::CryptoMessageFormat format; bool protectedHeaders = true; // copied from messagecomposer.cpp [[nodiscard]] bool binaryHint(Kleo::CryptoMessageFormat f) { switch (f) { case Kleo::SMIMEFormat: case Kleo::SMIMEOpaqueFormat: return true; default: case Kleo::OpenPGPMIMEFormat: case Kleo::InlineOpenPGPFormat: return false; } } [[nodiscard]] 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(SignJob) }; SignJob::SignJob(QObject *parent) : ContentJobBase(*new SignJobPrivate(this), parent) { } SignJob::~SignJob() = default; void SignJob::setContent(KMime::Content *content) { Q_D(SignJob); d->content = content; } void SignJob::setCryptoMessageFormat(Kleo::CryptoMessageFormat format) { Q_D(SignJob); // 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 SignJob::setSigningKeys(const std::vector &signers) { Q_D(SignJob); d->signers = signers; } void SignJob::setSkeletonMessage(KMime::Message *skeletonMessage) { Q_D(SignJob); d->skeletonMessage = skeletonMessage; } void SignJob::setProtectedHeaders(bool protectedHeaders) { Q_D(SignJob); d->protectedHeaders = protectedHeaders; } KMime::Content *SignJob::origContent() { Q_D(SignJob); return d->content; } void SignJob::doStart() { Q_D(SignJob); Q_ASSERT(d->resultContent == nullptr); // Not processed before. if (d->protectedHeaders && d->skeletonMessage && d->format & Kleo::OpenPGPMIMEFormat) { auto pJob = new ProtectedHeadersJob; pJob->setContent(d->content); pJob->setSkeletonMessage(d->skeletonMessage); pJob->setObvoscate(false); QObject::connect(pJob, &ProtectedHeadersJob::finished, this, [d, pJob](KJob *job) { if (job->error()) { return; } d->content = pJob->content(); }); if (!appendSubjob(pJob)) { qCWarning(EDITOR_LOG) << "Impossible to add subjob."; }; } ContentJobBase::doStart(); } void SignJob::slotResult(KJob *job) { if (error() || job->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 SignJob::process() { Q_D(SignJob); 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) { Q_ASSERT(d->subjobContents.size() == 1); d->content = d->subjobContents.constFirst(); } // d->resultContent = new KMime::Content; const QGpgME::Protocol *proto = nullptr; if (d->format & Kleo::AnyOpenPGP) { proto = QGpgME::openpgp(); } else if (d->format & Kleo::AnySMIME) { proto = QGpgME::smime(); } Q_ASSERT(proto); qCDebug(EDITOR_LOG) << "creating signJob from:" << proto->name() << proto->displayName(); // for now just do the main recipients - QByteArray signature; 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)) { // replace "From " and "--" at the beginning of lines // with encoded versions according to RfC 3156, 3 // Note: If any line begins with the string "From ", it is strongly // suggested that either the Quoted-Printable or Base64 MIME encoding // be applied. const auto encoding = d->content->contentTransferEncoding()->encoding(); if ((encoding == KMime::Headers::CEquPr || encoding == KMime::Headers::CE7Bit) && !d->content->contentType(false)) { QByteArray body = d->content->encodedBody(); bool changed = false; QList search; search.reserve(3); QList replacements; replacements.reserve(3); search << "From " << "from " << "-"; replacements << "From=20" << "from=20" << "=2D"; if (d->content->contentTransferEncoding()->encoding() == KMime::Headers::CE7Bit) { for (int i = 0, total = search.size(); i < total; ++i) { const auto pos = body.indexOf(search[i]); if (pos == 0 || (pos > 0 && body.at(pos - 1) == '\n')) { changed = true; break; } } if (changed) { d->content->contentTransferEncoding()->setEncoding(KMime::Headers::CEquPr); d->content->assemble(); body = d->content->encodedBody(); } } for (int i = 0; i < search.size(); ++i) { const auto pos = body.indexOf(search[i]); if (pos == 0 || (pos > 0 && body.at(pos - 1) == '\n')) { changed = true; body.replace(pos, search[i].size(), replacements[i]); } } if (changed) { qCDebug(EDITOR_LOG) << "Content changed"; d->content->setBody(body); d->content->contentTransferEncoding()->setDecoded(false); } } content = KMime::LFtoCRLF(d->content->encodedContent()); } else { // SMimeOpaque doesn't need LFtoCRLF, else it gets munged content = d->content->encodedContent(); } QGpgME::SignJob *job(proto->signJob(!d->binaryHint(d->format), d->format == Kleo::InlineOpenPGPFormat)); QObject::connect( job, &QGpgME::SignJob::result, this, [this, d](const GpgME::SigningResult &result, const QByteArray &signature, const QString &auditLogAsHtml, const GpgME::Error &auditLogError) { Q_UNUSED(auditLogAsHtml) Q_UNUSED(auditLogError) if (result.error().code()) { qCDebug(EDITOR_LOG) << "signing failed:" << result.error().asString(); // job->showErrorDialog( globalPart()->parentWidgetForGui() ); setError(result.error().code()); setErrorText(QString::fromLocal8Bit(result.error().asString())); emitResult(); return; } QByteArray signatureHashAlgo = result.createdSignature(0).hashAlgorithmAsString(); d->resultContent = MessageComposer::Util::composeHeadersAndBody(d->content, signature, d->format, true, signatureHashAlgo); emitResult(); }); const auto error = job->start(d->signers, content, d->signingMode(d->format)); if (error.code()) { job->deleteLater(); setError(error.code()); setErrorText(QString::fromLocal8Bit(error.asString())); emitResult(); } } #include "moc_signjob.cpp" diff --git a/client/editor/job/signjob.h b/client/editor/job/signjob.h index bb0d805..7dbe342 100644 --- a/client/editor/job/signjob.h +++ b/client/editor/job/signjob.h @@ -1,55 +1,55 @@ /* SPDX-FileCopyrightText: 2009 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.net SPDX-FileCopyrightText: 2009 Leo Franchi SPDX-License-Identifier: LGPL-2.0-or-later */ #pragma once +#include "../part/infopart.h" #include "contentjobbase.h" #include -#include "../part/infopart.h" #include #include namespace KMime { class Content; } namespace MessageComposer { class SignJobPrivate; /** Signs the contents of a message. Used as a subjob of CryptoMessage */ class SignJob : public ContentJobBase { Q_OBJECT public: explicit SignJob(QObject *parent = nullptr); ~SignJob() override; void setContent(KMime::Content *content); void setCryptoMessageFormat(Kleo::CryptoMessageFormat format); void setSigningKeys(const std::vector &signers); void setSkeletonMessage(KMime::Message *skeletonMessage); void setProtectedHeaders(bool protectedHeaders); [[nodiscard]] KMime::Content *origContent(); protected Q_SLOTS: void doStart() override; void slotResult(KJob *job) override; void process() override; private: Q_DECLARE_PRIVATE(SignJob) }; } diff --git a/client/editor/job/singlepartjob.cpp b/client/editor/job/singlepartjob.cpp index df0607e..e625673 100644 --- a/client/editor/job/singlepartjob.cpp +++ b/client/editor/job/singlepartjob.cpp @@ -1,181 +1,181 @@ /* SPDX-FileCopyrightText: 2009 Constantin Berzan SPDX-License-Identifier: LGPL-2.0-or-later */ #include "singlepartjob.h" -#include "contentjobbase_p.h" #include "../part/globalpart.h" +#include "contentjobbase_p.h" #include "editor_debug.h" #include #include #include using namespace MessageComposer; class MessageComposer::SinglepartJobPrivate : public ContentJobBasePrivate { public: SinglepartJobPrivate(SinglepartJob *qq) : ContentJobBasePrivate(qq) { } bool chooseCTE(); QByteArray data; KMime::Headers::ContentDescription *contentDescription = nullptr; KMime::Headers::ContentDisposition *contentDisposition = nullptr; KMime::Headers::ContentID *contentID = nullptr; KMime::Headers::ContentTransferEncoding *contentTransferEncoding = nullptr; KMime::Headers::ContentType *contentType = nullptr; Q_DECLARE_PUBLIC(SinglepartJob) }; bool SinglepartJobPrivate::chooseCTE() { Q_Q(SinglepartJob); auto allowed = KMime::encodingsForData(data); if (!q->globalPart()->is8BitAllowed()) { allowed.removeAll(KMime::Headers::CE8Bit); } #if 0 // TODO signing // In the following cases only QP and Base64 are allowed: // - the buffer will be OpenPGP/MIME signed and it contains trailing // whitespace (cf. RFC 3156) // - a line starts with "From " if ((willBeSigned && cf.hasTrailingWhitespace()) || cf.hasLeadingFrom()) { ret.removeAll(DwMime::kCte8bit); ret.removeAll(DwMime::kCte7bit); } #endif if (contentTransferEncoding) { // Specific CTE set. Check that our data fits in it. if (!allowed.contains(contentTransferEncoding->encoding())) { q->setError(JobBase::BugError); q->setErrorText( i18n("%1 Content-Transfer-Encoding cannot correctly encode this message.", KMime::nameForEncoding(contentTransferEncoding->encoding()))); return false; // TODO improve error message in case 8bit is requested but not allowed. } } else { // No specific CTE set. Choose the best one. Q_ASSERT(!allowed.isEmpty()); contentTransferEncoding = new KMime::Headers::ContentTransferEncoding; contentTransferEncoding->setEncoding(allowed.first()); } qCDebug(EDITOR_LOG) << "Settled on encoding" << KMime::nameForEncoding(contentTransferEncoding->encoding()); return true; } SinglepartJob::SinglepartJob(QObject *parent) : ContentJobBase(*new SinglepartJobPrivate(this), parent) { } SinglepartJob::~SinglepartJob() = default; QByteArray SinglepartJob::data() const { Q_D(const SinglepartJob); return d->data; } void SinglepartJob::setData(const QByteArray &data) { Q_D(SinglepartJob); d->data = data; } KMime::Headers::ContentDescription *SinglepartJob::contentDescription() { Q_D(SinglepartJob); if (!d->contentDescription) { d->contentDescription = new KMime::Headers::ContentDescription; } return d->contentDescription; } KMime::Headers::ContentDisposition *SinglepartJob::contentDisposition() { Q_D(SinglepartJob); if (!d->contentDisposition) { d->contentDisposition = new KMime::Headers::ContentDisposition; } return d->contentDisposition; } KMime::Headers::ContentID *SinglepartJob::contentID() { Q_D(SinglepartJob); if (!d->contentID) { d->contentID = new KMime::Headers::ContentID; } return d->contentID; } KMime::Headers::ContentTransferEncoding *SinglepartJob::contentTransferEncoding() { Q_D(SinglepartJob); if (!d->contentTransferEncoding) { d->contentTransferEncoding = new KMime::Headers::ContentTransferEncoding; } return d->contentTransferEncoding; } KMime::Headers::ContentType *SinglepartJob::contentType() { Q_D(SinglepartJob); if (!d->contentType) { d->contentType = new KMime::Headers::ContentType; } return d->contentType; } void SinglepartJob::process() { Q_D(SinglepartJob); Q_ASSERT(d->resultContent == nullptr); // Not processed before. d->resultContent = new KMime::Content; if (!d->chooseCTE()) { Q_ASSERT(error()); emitResult(); return; } // Set headers. if (d->contentDescription) { d->resultContent->setHeader(d->contentDescription); } if (d->contentDisposition) { d->resultContent->setHeader(d->contentDisposition); } if (d->contentID) { d->resultContent->setHeader(d->contentID); } Q_ASSERT(d->contentTransferEncoding); // chooseCTE() created it if it didn't exist. { d->resultContent->setHeader(d->contentTransferEncoding); } if (d->contentType) { d->resultContent->setHeader(d->contentType); } // Set data. d->resultContent->setBody(d->data); emitResult(); } #include "moc_singlepartjob.cpp" diff --git a/client/editor/job/skeletonmessagejob.cpp b/client/editor/job/skeletonmessagejob.cpp index 70f51b5..6473a9e 100644 --- a/client/editor/job/skeletonmessagejob.cpp +++ b/client/editor/job/skeletonmessagejob.cpp @@ -1,273 +1,273 @@ /* SPDX-FileCopyrightText: 2009 Constantin Berzan SPDX-License-Identifier: LGPL-2.0-or-later */ #include "skeletonmessagejob.h" -#include "jobbase_p.h" -#include "messagecomposersettings.h" #include "../part/globalpart.h" #include "../part/infopart.h" +#include "jobbase_p.h" +#include "messagecomposersettings.h" #include #include #include "editor_debug.h" #include #include using namespace MessageComposer; class MessageComposer::SkeletonMessageJobPrivate : public JobBasePrivate { public: SkeletonMessageJobPrivate(SkeletonMessageJob *qq) : JobBasePrivate(qq) { } void doStart(); // slot InfoPart *infoPart = nullptr; GlobalPart *globalPart = nullptr; KMime::Message *message = nullptr; Q_DECLARE_PUBLIC(SkeletonMessageJob) }; void SkeletonMessageJobPrivate::doStart() { Q_Q(SkeletonMessageJob); Q_ASSERT(infoPart); Q_ASSERT(message == nullptr); message = new KMime::Message; // From: { auto from = new KMime::Headers::From; KMime::Types::Mailbox address; address.fromUnicodeString(KEmailAddress::normalizeAddressesAndEncodeIdn(infoPart->from())); from->fromUnicodeString(QString::fromLatin1(address.as7BitString("utf-8")), "utf-8"); message->setHeader(from); } // To: { auto to = new KMime::Headers::To; QByteArray sTo; const QStringList lstTo = infoPart->to(); for (const QString &a : lstTo) { KMime::Types::Mailbox address; address.fromUnicodeString(KEmailAddress::normalizeAddressesAndEncodeIdn(a)); if (!sTo.isEmpty()) { sTo.append(","); } sTo.append(address.as7BitString("utf-8")); } to->fromUnicodeString(QString::fromLatin1(sTo), "utf-8"); message->setHeader(to); } // Reply To: if (!infoPart->replyTo().isEmpty()) { auto replyTo = new KMime::Headers::ReplyTo; const QStringList lstReplyTo = infoPart->replyTo(); QByteArray sReplyTo; for (const QString &a : lstReplyTo) { KMime::Types::Mailbox address; address.fromUnicodeString(KEmailAddress::normalizeAddressesAndEncodeIdn(a)); if (!sReplyTo.isEmpty()) { sReplyTo.append(","); } sReplyTo.append(address.as7BitString("utf-8")); } replyTo->fromUnicodeString(QString::fromLatin1(sReplyTo), "utf-8"); message->setHeader(replyTo); } // Cc: { auto cc = new KMime::Headers::Cc; QByteArray sCc; const QStringList lstCc = infoPart->cc(); for (const QString &a : lstCc) { KMime::Types::Mailbox address; address.fromUnicodeString(KEmailAddress::normalizeAddressesAndEncodeIdn(a)); if (!sCc.isEmpty()) { sCc.append(","); } sCc.append(address.as7BitString("utf-8")); } cc->fromUnicodeString(QString::fromLatin1(sCc), "utf-8"); message->setHeader(cc); } // Bcc: { auto bcc = new KMime::Headers::Bcc; QByteArray sBcc; const QStringList lstBcc = infoPart->bcc(); for (const QString &a : lstBcc) { KMime::Types::Mailbox address; address.fromUnicodeString(KEmailAddress::normalizeAddressesAndEncodeIdn(a)); if (!sBcc.isEmpty()) { sBcc.append(","); } sBcc.append(address.as7BitString("utf-8")); } bcc->fromUnicodeString(QString::fromLatin1(sBcc), "utf-8"); message->setHeader(bcc); } // Subject: { auto subject = new KMime::Headers::Subject; subject->fromUnicodeString(infoPart->subject(), "utf-8"); // TODO should we be more specific about the charset? message->setHeader(subject); } // Date: { auto date = new KMime::Headers::Date; date->setDateTime(QDateTime::currentDateTime()); message->setHeader(date); } // Fcc: if (!infoPart->fcc().isEmpty()) { auto header = new KMime::Headers::Generic("X-KMail-Fcc"); header->fromUnicodeString(infoPart->fcc(), "utf-8"); message->setHeader(header); } // Message-ID { auto messageId = new KMime::Headers::MessageID(); QByteArray fqdn; if (MessageComposer::MessageComposerSettings::self()->useCustomMessageIdSuffix()) { fqdn = QUrl::toAce(MessageComposer::MessageComposerSettings::self()->customMsgIDSuffix()); } if (fqdn.isEmpty()) { fqdn = QUrl::toAce(QHostInfo::localHostName()); } if (fqdn.isEmpty()) { qCWarning(EDITOR_LOG) << "Unable to generate a Message-ID, falling back to 'localhost.localdomain'."; fqdn = "local.domain"; } messageId->generate(fqdn); message->setHeader(messageId); } // Extras const KMime::Headers::Base::List extraHeaders = infoPart->extraHeaders(); for (KMime::Headers::Base *extra : extraHeaders) { const QByteArray headerType(extra->type()); auto copyHeader = KMime::Headers::createHeader(headerType); if (!copyHeader) { copyHeader = new KMime::Headers::Generic(headerType.constData(), headerType.size()); } copyHeader->from7BitString(extra->as7BitString(false)); message->setHeader(copyHeader); } // Request Delivery Confirmation { if (globalPart->requestDeleveryConfirmation()) { // TODO fix me multi address const QString addr = infoPart->replyTo().isEmpty() ? infoPart->from() : infoPart->replyTo().at(0); auto requestDeleveryConfirmation = new KMime::Headers::Generic("Return-Receipt-To"); requestDeleveryConfirmation->fromUnicodeString(addr, "utf-8"); message->setHeader(requestDeleveryConfirmation); } } // MDN { if (globalPart->MDNRequested()) { // TODO fix me multi address const QString addr = infoPart->replyTo().isEmpty() ? infoPart->from() : infoPart->replyTo().at(0); auto mdn = new KMime::Headers::Generic("Disposition-Notification-To"); mdn->fromUnicodeString(addr, "utf-8"); message->setHeader(mdn); } } // Urgent header if (infoPart->urgent()) { auto urg1 = new KMime::Headers::Generic("X-PRIORITY"); urg1->fromUnicodeString(QStringLiteral("2 (High)"), "utf-8"); auto urg2 = new KMime::Headers::Generic("Priority"); urg2->fromUnicodeString(QStringLiteral("urgent"), "utf-8"); message->setHeader(urg1); message->setHeader(urg2); } // In-Reply-To if (!infoPart->inReplyTo().isEmpty()) { auto header = new KMime::Headers::InReplyTo; header->fromUnicodeString(infoPart->inReplyTo(), "utf-8"); message->setHeader(header); } // References if (!infoPart->references().isEmpty()) { auto header = new KMime::Headers::References; header->fromUnicodeString(infoPart->references(), "utf-8"); message->setHeader(header); } q->emitResult(); // Success. } SkeletonMessageJob::SkeletonMessageJob(InfoPart *infoPart, GlobalPart *globalPart, QObject *parent) : JobBase(*new SkeletonMessageJobPrivate(this), parent) { Q_D(SkeletonMessageJob); d->infoPart = infoPart; d->globalPart = globalPart; } SkeletonMessageJob::~SkeletonMessageJob() = default; InfoPart *SkeletonMessageJob::infoPart() const { Q_D(const SkeletonMessageJob); return d->infoPart; } void SkeletonMessageJob::setInfoPart(InfoPart *part) { Q_D(SkeletonMessageJob); d->infoPart = part; } GlobalPart *SkeletonMessageJob::globalPart() const { Q_D(const SkeletonMessageJob); return d->globalPart; } void SkeletonMessageJob::setGlobalPart(GlobalPart *part) { Q_D(SkeletonMessageJob); d->globalPart = part; } KMime::Message *SkeletonMessageJob::message() const { Q_D(const SkeletonMessageJob); return d->message; } void SkeletonMessageJob::start() { Q_D(SkeletonMessageJob); d->doStart(); } #include "moc_skeletonmessagejob.cpp" diff --git a/client/editor/mailtemplates.cpp b/client/editor/mailtemplates.cpp index c124fcb..e21b6a2 100644 --- a/client/editor/mailtemplates.cpp +++ b/client/editor/mailtemplates.cpp @@ -1,935 +1,919 @@ /* Copyright (c) 2009 Constantin Berzan Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Copyright (c) 2010 Leo Franchi Copyright (c) 2017 Christian Mollekopf This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "mailtemplates.h" -#include +#include #include -#include #include +#include +#include +#include #include #include -#include -#include -#include +#include #include #include #include using namespace Qt::Literals::StringLiterals; QDebug operator<<(QDebug dbg, const KMime::Types::Mailbox &mb) { dbg << mb.addrSpec().asString(); return dbg; } -namespace KMime { - namespace Types { +namespace KMime +{ +namespace Types +{ static bool operator==(const KMime::Types::AddrSpec &left, const KMime::Types::AddrSpec &right) { return (left.asString() == right.asString()); } static bool operator==(const KMime::Types::Mailbox &left, const KMime::Types::Mailbox &right) { return (left.addrSpec().asString() == right.addrSpec().asString()); } - } +} - Message* contentToMessage(Content* content) { - content->assemble(); - const auto encoded = content->encodedContent(); +Message *contentToMessage(Content *content) +{ + content->assemble(); + const auto encoded = content->encodedContent(); - auto message = new Message(); - message->setContent(encoded); - message->parse(); + auto message = new Message(); + message->setContent(encoded); + message->parse(); - return message; - } + return message; +} } static KMime::Types::Mailbox::List stripMyAddressesFromAddressList(const KMime::Types::Mailbox::List &list, const KMime::Types::AddrSpecList me) { KMime::Types::Mailbox::List addresses(list); for (KMime::Types::Mailbox::List::Iterator it = addresses.begin(); it != addresses.end();) { if (me.contains(it->addrSpec())) { it = addresses.erase(it); } else { ++it; } } return addresses; } static QString toPlainText(const QString &s) { QTextDocument doc; doc.setHtml(s); return doc.toPlainText(); } QString replacePrefixes(const QString &str, const QStringList &prefixRegExps, const QString &newPrefix) { // construct a big regexp that // 1. is anchored to the beginning of str (sans whitespace) // 2. matches at least one of the part regexps in prefixRegExps const QString bigRegExp = QStringLiteral("^(?:\\s+|(?:%1))+\\s*").arg(prefixRegExps.join(QStringLiteral(")|(?:"))); QRegularExpression rx(bigRegExp, QRegularExpression::CaseInsensitiveOption); if (!rx.isValid()) { - qWarning() << "bigRegExp = \"" - << bigRegExp << "\"\n" - << "prefix regexp is invalid!"; + qWarning() << "bigRegExp = \"" << bigRegExp << "\"\n" + << "prefix regexp is invalid!"; qWarning() << "Error: " << rx.errorString() << rx; Q_ASSERT(false); return str; } QString tmp = str; - //We expect a match at the beginning of the string + // We expect a match at the beginning of the string QRegularExpressionMatch match; if (tmp.indexOf(rx, 0, &match) == 0) { return tmp.replace(0, match.capturedLength(), newPrefix + QLatin1Char(' ')); } - //No match, we just prefix the newPrefix + // No match, we just prefix the newPrefix return newPrefix + u' ' + str; } const QStringList getForwardPrefixes() { - //See https://en.wikipedia.org/wiki/List_of_email_subject_abbreviations + // See https://en.wikipedia.org/wiki/List_of_email_subject_abbreviations QStringList list; - //We want to be able to potentially reply to a variety of languages, so only translating is not enough + // We want to be able to potentially reply to a variety of languages, so only translating is not enough list << i18nc("FWD abbreviation for forwarded in emails", "fwd"); list << u"fwd"_s; list << u"fw"_s; list << u"wg"_s; list << u"vs"_s; list << u"tr"_s; list << u"rv"_s; list << u"enc"_s; return list; } - static QString forwardSubject(const QString &s) { - //The standandard prefix + // The standandard prefix const QString localPrefix = QStringLiteral("FW:"); QStringList forwardPrefixes; for (const auto &prefix : getForwardPrefixes()) { forwardPrefixes << prefix + QStringLiteral("\\s*:"); } return replacePrefixes(s, forwardPrefixes, localPrefix); } static QStringList getReplyPrefixes() { - //See https://en.wikipedia.org/wiki/List_of_email_subject_abbreviations + // See https://en.wikipedia.org/wiki/List_of_email_subject_abbreviations QStringList list; - //We want to be able to potentially reply to a variety of languages, so only translating is not enough + // We want to be able to potentially reply to a variety of languages, so only translating is not enough list << i18nc("RE abbreviation for reply in emails", "re"); list << u"re"_s; list << u"aw"_s; list << u"sv"_s; list << u"antw"_s; list << u"ref"_s; return list; } static QString replySubject(const QString &s) { - //The standandard prefix (latin for "in re", in matter of) + // The standandard prefix (latin for "in re", in matter of) const QString localPrefix = QStringLiteral("RE:"); QStringList replyPrefixes; for (const auto &prefix : getReplyPrefixes()) { replyPrefixes << prefix + u"\\s*:"_s; replyPrefixes << prefix + u"\\[.+\\]:"_s; replyPrefixes << prefix + u"\\d+:"_s; } return replacePrefixes(s, replyPrefixes, localPrefix); } static QByteArray getRefStr(const QByteArray &references, const QByteArray &messageId) { QByteArray firstRef, lastRef, refStr{references.trimmed()}, retRefStr; int i, j; if (refStr.isEmpty()) { return messageId; } i = refStr.indexOf('<'); j = refStr.indexOf('>'); firstRef = refStr.mid(i, j - i + 1); if (!firstRef.isEmpty()) { retRefStr = firstRef + ' '; } i = refStr.lastIndexOf('<'); j = refStr.lastIndexOf('>'); lastRef = refStr.mid(i, j - i + 1); if (!lastRef.isEmpty() && lastRef != firstRef) { retRefStr += lastRef + ' '; } retRefStr += messageId; return retRefStr; } KMime::Content *createPlainPartContent(const QString &plainBody, KMime::Content *parent = nullptr) { KMime::Content *textPart = new KMime::Content(parent); textPart->contentType()->setMimeType("text/plain"); - //FIXME This is supposed to select a charset out of the available charsets that contains all necessary characters to render the text - // QTextCodec *charset = selectCharset(m_charsets, plainBody); - // textPart->contentType()->setCharset(charset->name()); + // FIXME This is supposed to select a charset out of the available charsets that contains all necessary characters to render the text + // QTextCodec *charset = selectCharset(m_charsets, plainBody); + // textPart->contentType()->setCharset(charset->name()); textPart->contentType()->setCharset("utf-8"); textPart->contentTransferEncoding()->setEncoding(KMime::Headers::CE8Bit); textPart->fromUnicodeString(plainBody); return textPart; } KMime::Content *createMultipartAlternativeContent(const QString &plainBody, const QString &htmlBody, KMime::Message *parent = nullptr) { KMime::Content *multipartAlternative = new KMime::Content(parent); multipartAlternative->contentType()->setMimeType("multipart/alternative"); multipartAlternative->contentType()->setBoundary(KMime::multiPartBoundary()); KMime::Content *textPart = createPlainPartContent(plainBody, multipartAlternative); multipartAlternative->appendContent(textPart); KMime::Content *htmlPart = new KMime::Content(multipartAlternative); htmlPart->contentType()->setMimeType("text/html"); - //FIXME This is supposed to select a charset out of the available charsets that contains all necessary characters to render the text - // QTextCodec *charset = selectCharset(m_charsets, htmlBody); - // htmlPart->contentType()->setCharset(charset->name()); + // FIXME This is supposed to select a charset out of the available charsets that contains all necessary characters to render the text + // QTextCodec *charset = selectCharset(m_charsets, htmlBody); + // htmlPart->contentType()->setCharset(charset->name()); htmlPart->contentType()->setCharset("utf-8"); htmlPart->contentTransferEncoding()->setEncoding(KMime::Headers::CE8Bit); htmlPart->fromUnicodeString(htmlBody); multipartAlternative->appendContent(htmlPart); return multipartAlternative; } KMime::Content *createMultipartMixedContent(QVector contents) { KMime::Content *multiPartMixed = new KMime::Content(); multiPartMixed->contentType()->setMimeType("multipart/mixed"); multiPartMixed->contentType()->setBoundary(KMime::multiPartBoundary()); for (const auto &content : contents) { multiPartMixed->appendContent(content); } return multiPartMixed; } QString plainToHtml(const QString &body) { QString str = body; str = str.toHtmlEscaped(); str.replace(u'\n', u"
\n"_s); return str; } -//TODO implement this function using a DOM tree parser +// TODO implement this function using a DOM tree parser void makeValidHtml(QString &body, const QString &headElement) { QRegularExpression regEx(u""_s, QRegularExpression::InvertedGreedinessOption); if (!body.isEmpty() && !body.contains(regEx)) { regEx.setPattern(u""_s); if (!body.contains(regEx)) { body = u""_s + body + u"
"_s; } regEx.setPattern(u""_s); if (!body.contains(regEx)) { body = u""_s + headElement + u""_s + body; } body = u""_s + body + u""_s; } } -//FIXME strip signature works partially for HTML mails +// FIXME strip signature works partially for HTML mails static QString stripSignature(const QString &msg) { // Following RFC 3676, only > before -- // I prefer to not delete a SB instead of delete good mail content. // We expect no CRLF from the ObjectTreeParser. The regex won't handle it. if (msg.contains(QStringLiteral("\r\n"))) { qWarning() << "Message contains CRLF, but shouldn't: " << msg; Q_ASSERT(false); } static const QRegularExpression sbDelimiterSearch(u"(^|\n)[> ]*-- \n"_s); // The regular expression to look for prefix change static const QRegularExpression commonReplySearch(u"^[ ]*>"_s); QString res = msg; int posDeletingStart = 1; // to start looking at 0 // While there are SB delimiters (start looking just before the deleted SB) while ((posDeletingStart = res.indexOf(sbDelimiterSearch, posDeletingStart - 1)) >= 0) { QString prefix; // the current prefix QString line; // the line to check if is part of the SB int posNewLine = -1; // Look for the SB beginning int posSignatureBlock = res.indexOf(u'-', posDeletingStart); // The prefix before "-- "$ if (res.at(posDeletingStart) == u'\n') { ++posDeletingStart; } prefix = res.mid(posDeletingStart, posSignatureBlock - posDeletingStart); posNewLine = res.indexOf(QLatin1Char('\n'), posSignatureBlock) + 1; // now go to the end of the SB while (posNewLine < res.size() && posNewLine > 0) { // handle the undefined case for mid ( x , -n ) where n>1 int nextPosNewLine = res.indexOf(QLatin1Char('\n'), posNewLine); if (nextPosNewLine < 0) { nextPosNewLine = posNewLine - 1; } line = res.mid(posNewLine, nextPosNewLine - posNewLine); // check when the SB ends: // * does not starts with prefix or // * starts with prefix+(any substring of prefix) - if ((prefix.isEmpty() && line.indexOf(commonReplySearch) < 0) || - (!prefix.isEmpty() && line.startsWith(prefix) && - line.mid(prefix.size()).indexOf(commonReplySearch) < 0)) { + if ((prefix.isEmpty() && line.indexOf(commonReplySearch) < 0) + || (!prefix.isEmpty() && line.startsWith(prefix) && line.mid(prefix.size()).indexOf(commonReplySearch) < 0)) { posNewLine = res.indexOf(QLatin1Char('\n'), posNewLine) + 1; } else { - break; // end of the SB + break; // end of the SB } } // remove the SB or truncate when is the last SB if (posNewLine > 0) { res.remove(posDeletingStart, posNewLine - posDeletingStart); } else { res.truncate(posDeletingStart); } } return res; } static void plainMessageText(const QString &plainTextContent, const QString &htmlContent, const std::function &callback) { const auto result = plainTextContent.isEmpty() ? toPlainText(htmlContent) : plainTextContent; callback(result); } void htmlMessageText(const QString &plainTextContent, const QString &htmlContent, const std::function &callback) { QString htmlElement = htmlContent; - if (htmlElement.isEmpty()) { //plain mails only + if (htmlElement.isEmpty()) { // plain mails only QString htmlReplace = plainTextContent.toHtmlEscaped(); htmlReplace = htmlReplace.replace(QStringLiteral("\n"), QStringLiteral("
")); htmlElement = QStringLiteral("%1\n").arg(htmlReplace); } QDomDocument document; document.setContent(htmlElement); - QString body; QTextStream bodyStream(&body); QString head; QTextStream headStream(&head); const auto bodies = document.elementsByTagName(u"body"_s); const auto heads = document.elementsByTagName(u"head"_s); if (bodies.isEmpty()) { body = htmlElement; } else { bodies.item(0).save(bodyStream, 2); } if (!heads.isEmpty()) { heads.item(0).save(headStream, 2); } callback(body, head); } QString formatQuotePrefix(const QString &wildString, const QString &fromDisplayString) { QString result; if (wildString.isEmpty()) { return wildString; } unsigned int strLength(wildString.length()); for (uint i = 0; i < strLength;) { QChar ch = wildString[i++]; if (ch == QLatin1Char('%') && i < strLength) { ch = wildString[i++]; switch (ch.toLatin1()) { case 'f': { // sender's initals if (fromDisplayString.isEmpty()) { break; } uint j = 0; const unsigned int strLength(fromDisplayString.length()); for (; j < strLength && fromDisplayString[j] > QLatin1Char(' '); ++j) ; for (; j < strLength && fromDisplayString[j] <= QLatin1Char(' '); ++j) ; result += fromDisplayString[0]; if (j < strLength && fromDisplayString[j] > QLatin1Char(' ')) { result += fromDisplayString[j]; } else if (strLength > 1) { if (fromDisplayString[1] > QLatin1Char(' ')) { result += fromDisplayString[1]; } } - } - break; + } break; case '_': result += QLatin1Char(' '); break; case '%': result += QLatin1Char('%'); break; default: result += QLatin1Char('%'); result += ch; break; } } else { result += ch; } } return result; } QString quotedPlainText(const QString &selection, const QString &fromDisplayString) { QString content = selection; // Remove blank lines at the beginning: const int firstNonWS = content.indexOf(QRegularExpression(u"\\S"_s)); const int lineStart = content.lastIndexOf(QLatin1Char('\n'), firstNonWS); if (lineStart >= 0) { content.remove(0, static_cast(lineStart)); } const auto quoteString = QStringLiteral("> "); const QString indentStr = formatQuotePrefix(quoteString, fromDisplayString); - //FIXME - // if (TemplateParserSettings::self()->smartQuote() && mWrap) { - // content = MessageCore::StringUtil::smartQuote(content, mColWrap - indentStr.length()); - // } + // FIXME + // if (TemplateParserSettings::self()->smartQuote() && mWrap) { + // content = MessageCore::StringUtil::smartQuote(content, mColWrap - indentStr.length()); + // } content.replace(QLatin1Char('\n'), QLatin1Char('\n') + indentStr); content.prepend(indentStr); content += QLatin1Char('\n'); return content; } QString quotedHtmlText(const QString &selection) { QString content = selection; - //TODO 1) look for all the variations of
and remove the blank lines - //2) implement vertical bar for quoted HTML mail. - //3) After vertical bar is implemented, If a user wants to edit quoted message, - // then the
tags below should open and close as when required. + // TODO 1) look for all the variations of
and remove the blank lines + // 2) implement vertical bar for quoted HTML mail. + // 3) After vertical bar is implemented, If a user wants to edit quoted message, + // then the
tags below should open and close as when required. - //Add blockquote tag, so that quoted message can be differentiated from normal message + // Add blockquote tag, so that quoted message can be differentiated from normal message content = QLatin1String("
") + content + QLatin1String("
"); return content; } -enum ReplyStrategy { - ReplyList, - ReplySmart, - ReplyAll, - ReplyAuthor, - ReplyNone -}; - +enum ReplyStrategy { ReplyList, ReplySmart, ReplyAll, ReplyAuthor, ReplyNone }; static QByteArray as7BitString(const KMime::Headers::Base *h) { if (h) { return h->as7BitString(false); } return {}; } - static QString asUnicodeString(const KMime::Headers::Base *h) { if (h) { return h->asUnicodeString(); } return {}; } static KMime::Types::Mailbox::List getMailingListAddresses(const KMime::Headers::Base *listPostHeader) { KMime::Types::Mailbox::List mailingListAddresses; const QString listPost = asUnicodeString(listPostHeader); if (listPost.contains(QStringLiteral("mailto:"), Qt::CaseInsensitive)) { static QRegularExpression rx(QStringLiteral("]+)@([^>]+)>"), QRegularExpression::CaseInsensitiveOption); QRegularExpressionMatch match; - if (listPost.indexOf(rx, 0, &match) != -1) { // matched + if (listPost.indexOf(rx, 0, &match) != -1) { // matched KMime::Types::Mailbox mailbox; mailbox.fromUnicodeString(match.captured(1) + u'@' + match.captured(2)); mailingListAddresses << mailbox; } } return mailingListAddresses; } struct RecipientMailboxes { KMime::Types::Mailbox::List to; KMime::Types::Mailbox::List cc; }; -static RecipientMailboxes getRecipients(const KMime::Types::Mailbox::List &from, const KMime::Types::Mailbox::List &to, const KMime::Types::Mailbox::List &cc, const KMime::Types::Mailbox::List &replyToList, const KMime::Types::Mailbox::List &mailingListAddresses, const KMime::Types::AddrSpecList &me) +static RecipientMailboxes getRecipients(const KMime::Types::Mailbox::List &from, + const KMime::Types::Mailbox::List &to, + const KMime::Types::Mailbox::List &cc, + const KMime::Types::Mailbox::List &replyToList, + const KMime::Types::Mailbox::List &mailingListAddresses, + const KMime::Types::AddrSpecList &me) { KMime::Types::Mailbox::List toList; KMime::Types::Mailbox::List ccList; - auto listContainsMe = [&] (const KMime::Types::Mailbox::List &list) { + auto listContainsMe = [&](const KMime::Types::Mailbox::List &list) { for (const auto &m : me) { KMime::Types::Mailbox mailbox; mailbox.setAddress(m); if (list.contains(mailbox)) { return true; } } return false; }; if (listContainsMe(from)) { // sender seems to be one of our own identities, so we assume that this // is a reply to a "sent" mail where the users wants to add additional // information for the recipient. return {to, cc}; } KMime::Types::Mailbox::List recipients; KMime::Types::Mailbox::List ccRecipients; // add addresses from the Reply-To header to the list of recipients if (!replyToList.isEmpty()) { recipients = replyToList; // strip all possible mailing list addresses from the list of Reply-To addresses for (const KMime::Types::Mailbox &mailbox : std::as_const(mailingListAddresses)) { for (const KMime::Types::Mailbox &recipient : std::as_const(recipients)) { if (mailbox == recipient) { recipients.removeAll(recipient); } } } } if (!mailingListAddresses.isEmpty()) { // this is a mailing list message if (recipients.isEmpty() && !from.isEmpty()) { // The sender didn't set a Reply-to address, so we add the From // address to the list of CC recipients. ccRecipients += from; qDebug() << "Added" << from << "to the list of CC recipients"; } // if it is a mailing list, add the posting address recipients.prepend(mailingListAddresses[0]); } else { // this is a normal message if (recipients.isEmpty() && !from.isEmpty()) { // in case of replying to a normal message only then add the From // address to the list of recipients if there was no Reply-to address recipients += from; qDebug() << "Added" << from << "to the list of recipients"; } } // strip all my addresses from the list of recipients toList = stripMyAddressesFromAddressList(recipients, me); // merge To header and CC header into a list of CC recipients - auto appendToCcRecipients = [&](const KMime::Types::Mailbox::List & list) { + auto appendToCcRecipients = [&](const KMime::Types::Mailbox::List &list) { for (const KMime::Types::Mailbox &mailbox : list) { if (!recipients.contains(mailbox) && !ccRecipients.contains(mailbox)) { ccRecipients += mailbox; qDebug() << "Added" << mailbox.prettyAddress() << "to the list of CC recipients"; } } }; appendToCcRecipients(to); appendToCcRecipients(cc); if (!ccRecipients.isEmpty()) { // strip all my addresses from the list of CC recipients ccRecipients = stripMyAddressesFromAddressList(ccRecipients, me); // in case of a reply to self, toList might be empty. if that's the case // then propagate a cc recipient to To: (if there is any). if (toList.isEmpty() && !ccRecipients.isEmpty()) { toList << ccRecipients.at(0); ccRecipients.pop_front(); } ccList = ccRecipients; } if (toList.isEmpty() && !recipients.isEmpty()) { // reply to self without other recipients toList << recipients.at(0); } return {toList, ccList}; } -void MailTemplates::reply(const KMime::Message::Ptr &origMsg, const std::function &callback, const KMime::Types::AddrSpecList &me) +void MailTemplates::reply(const KMime::Message::Ptr &origMsg, + const std::function &callback, + const KMime::Types::AddrSpecList &me) { - //FIXME + // FIXME const bool alwaysPlain = true; // Decrypt what we have to MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(origMsg.data()); otp.decryptAndVerify(); auto partList = otp.collectContentParts(); if (partList.isEmpty()) { Q_ASSERT(false); return; } auto part = partList[0]; Q_ASSERT(part); // Prepare the reply message KMime::Message::Ptr msg(new KMime::Message); msg->removeHeader(); msg->removeHeader(); msg->contentType(true)->setMimeType("text/plain"); msg->contentType()->setCharset("utf-8"); auto getMailboxes = [](const KMime::Headers::Base *h) -> KMime::Types::Mailbox::List { if (h) { - return static_cast(h)->mailboxes(); + return static_cast(h)->mailboxes(); } return {}; }; - auto fromHeader = static_cast(part->header(KMime::Headers::From::staticType())); - const auto recipients = getRecipients( - fromHeader ? fromHeader->mailboxes() : KMime::Types::Mailbox::List{}, - getMailboxes(part->header(KMime::Headers::To::staticType())), - getMailboxes(part->header(KMime::Headers::Cc::staticType())), - getMailboxes(part->header(KMime::Headers::ReplyTo::staticType())), - getMailingListAddresses(part->header("List-Post")), - me - ); + auto fromHeader = static_cast(part->header(KMime::Headers::From::staticType())); + const auto recipients = getRecipients(fromHeader ? fromHeader->mailboxes() : KMime::Types::Mailbox::List{}, + getMailboxes(part->header(KMime::Headers::To::staticType())), + getMailboxes(part->header(KMime::Headers::Cc::staticType())), + getMailboxes(part->header(KMime::Headers::ReplyTo::staticType())), + getMailingListAddresses(part->header("List-Post")), + me); for (const auto &mailbox : recipients.to) { msg->to()->addAddress(mailbox); } for (const auto &mailbox : recipients.cc) { msg->cc(true)->addAddress(mailbox); } const auto messageId = as7BitString(part->header(KMime::Headers::MessageID::staticType())); const QByteArray refStr = getRefStr(as7BitString(part->header(KMime::Headers::References::staticType())), messageId); if (!refStr.isEmpty()) { msg->references()->fromUnicodeString(QString::fromLocal8Bit(refStr), "utf-8"); } - //In-Reply-To = original msg-id + // In-Reply-To = original msg-id msg->inReplyTo()->from7BitString(messageId); - const auto subjectHeader = part->header(KMime::Headers::Subject::staticType()); msg->subject()->fromUnicodeString(replySubject(asUnicodeString(subjectHeader)), "utf-8"); auto definedLocale = QLocale::system(); - //Add quoted body + // Add quoted body QString plainBody; QString htmlBody; - //On $datetime you wrote: - auto dateHeader = static_cast(part->header(KMime::Headers::Date::staticType())); + // On $datetime you wrote: + auto dateHeader = static_cast(part->header(KMime::Headers::Date::staticType())); const QDateTime date = dateHeader ? dateHeader->dateTime() : QDateTime{}; - const auto dateTimeString = QStringLiteral("%1 %2").arg(definedLocale.toString(date.date(), QLocale::LongFormat), definedLocale.toString(date.time(), QLocale::LongFormat)); + const auto dateTimeString = + QStringLiteral("%1 %2").arg(definedLocale.toString(date.date(), QLocale::LongFormat), definedLocale.toString(date.time(), QLocale::LongFormat)); const auto onDateYouWroteLine = i18nc("Reply header", "On %1 you wrote:\n", dateTimeString); plainBody.append(onDateYouWroteLine); htmlBody.append(plainToHtml(onDateYouWroteLine)); const auto plainTextContent = otp.plainTextContent(); const auto htmlContent = otp.htmlContent(); - plainMessageText(plainTextContent, htmlContent, [=] (const QString &body) { + plainMessageText(plainTextContent, htmlContent, [=](const QString &body) { QString result = stripSignature(body); - //Quoted body - result = quotedPlainText(result, fromHeader ? fromHeader->displayString() : QString{}); + // Quoted body + result = quotedPlainText(result, fromHeader ? fromHeader->displayString() : QString{}); if (result.endsWith(u'\n')) { result.chop(1); } - //The plain body is complete + // The plain body is complete QString plainBodyResult = plainBody + result; - htmlMessageText(plainTextContent, htmlContent, [=] (const QString &body, const QString &headElement) { + htmlMessageText(plainTextContent, htmlContent, [=](const QString &body, const QString &headElement) { QString result = stripSignature(body); - //The html body is complete + // The html body is complete const auto htmlBodyResult = [&]() { if (!alwaysPlain) { QString htmlBodyResult = htmlBody + quotedHtmlText(result); makeValidHtml(htmlBodyResult, headElement); return htmlBodyResult; } return QString{}; }(); - //Assemble the message + // Assemble the message msg->contentType()->clear(); // to get rid of old boundary - KMime::Content *const mainTextPart = - htmlBodyResult.isEmpty() ? - createPlainPartContent(plainBodyResult, msg.data()) : - createMultipartAlternativeContent(plainBodyResult, htmlBodyResult, msg.data()); + KMime::Content *const mainTextPart = htmlBodyResult.isEmpty() ? createPlainPartContent(plainBodyResult, msg.data()) + : createMultipartAlternativeContent(plainBodyResult, htmlBodyResult, msg.data()); mainTextPart->assemble(); msg->setBody(mainTextPart->encodedBody()); msg->setHeader(mainTextPart->contentType()); msg->setHeader(mainTextPart->contentTransferEncoding()); - //FIXME this does more harm than good right now. + // FIXME this does more harm than good right now. msg->assemble(); callback(msg); }); }); } -void MailTemplates::forward(const KMime::Message::Ptr &origMsg, - const std::function &callback) +void MailTemplates::forward(const KMime::Message::Ptr &origMsg, const std::function &callback) { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(origMsg.data()); otp.decryptAndVerify(); - KMime::Message::Ptr wrapperMsg(new KMime::Message); wrapperMsg->to()->clear(); wrapperMsg->cc()->clear(); // Decrypt the original message, it will be encrypted again in the composer // for the right recipient KMime::Message::Ptr forwardedMessage(new KMime::Message()); if (isEncrypted(origMsg.data())) { qDebug() << "Original message was encrypted, decrypting it"; auto htmlContent = otp.htmlContent(); KMime::Content *recreatedMsg = - htmlContent.isEmpty() ? createPlainPartContent(otp.plainTextContent()) : - createMultipartAlternativeContent(otp.plainTextContent(), htmlContent); + htmlContent.isEmpty() ? createPlainPartContent(otp.plainTextContent()) : createMultipartAlternativeContent(otp.plainTextContent(), htmlContent); KMime::Message::Ptr tmpForwardedMessage; auto attachments = otp.collectAttachmentParts(); if (!attachments.isEmpty()) { QVector contents = {recreatedMsg}; for (const auto &attachment : std::as_const(attachments)) { - //Copy the node, to avoid deleting the parts node. + // Copy the node, to avoid deleting the parts node. auto c = new KMime::Content; c->setContent(attachment->node()->encodedContent()); c->parse(); contents.append(c); } auto msg = createMultipartMixedContent(contents); tmpForwardedMessage.reset(KMime::contentToMessage(msg)); } else { tmpForwardedMessage.reset(KMime::contentToMessage(recreatedMsg)); } origMsg->contentType()->fromUnicodeString(tmpForwardedMessage->contentType()->asUnicodeString(), "utf-8"); origMsg->assemble(); forwardedMessage->setHead(origMsg->head()); forwardedMessage->setBody(tmpForwardedMessage->encodedBody()); forwardedMessage->parse(); } else { qDebug() << "Original message was not encrypted, using it as-is"; forwardedMessage = origMsg; } auto partList = otp.collectContentParts(); if (partList.isEmpty()) { Q_ASSERT(false); callback({}); return; } auto part = partList[0]; Q_ASSERT(part); const auto subjectHeader = part->header(KMime::Headers::Subject::staticType()); const auto subject = asUnicodeString(subjectHeader); - const QByteArray refStr = getRefStr( - as7BitString(part->header(KMime::Headers::References::staticType())), - as7BitString(part->header(KMime::Headers::MessageID::staticType())) - ); + const QByteArray refStr = + getRefStr(as7BitString(part->header(KMime::Headers::References::staticType())), as7BitString(part->header(KMime::Headers::MessageID::staticType()))); wrapperMsg->subject()->fromUnicodeString(forwardSubject(subject), "utf-8"); if (!refStr.isEmpty()) { wrapperMsg->references()->fromUnicodeString(QString::fromLocal8Bit(refStr), "utf-8"); } KMime::Content *fwdAttachment = new KMime::Content; fwdAttachment->contentDisposition()->setDisposition(KMime::Headers::CDinline); fwdAttachment->contentType()->setMimeType("message/rfc822"); fwdAttachment->contentDisposition()->setFilename(subject + u".eml"_s); fwdAttachment->setBody(KMime::CRLFtoLF(forwardedMessage->encodedContent(false))); wrapperMsg->appendContent(fwdAttachment); wrapperMsg->assemble(); callback(wrapperMsg); } QString MailTemplates::plaintextContent(const KMime::Message::Ptr &msg) { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(msg.data()); otp.decryptAndVerify(); const auto plain = otp.plainTextContent(); if (plain.isEmpty()) { - //Maybe not as good as the webengine version, but works at least for simple html content + // Maybe not as good as the webengine version, but works at least for simple html content return toPlainText(otp.htmlContent()); } return plain; } QString MailTemplates::body(const KMime::Message::Ptr &msg, bool &isHtml) { MimeTreeParser::ObjectTreeParser otp; otp.parseObjectTree(msg.data()); otp.decryptAndVerify(); const auto html = otp.htmlContent(); if (html.isEmpty()) { isHtml = false; return otp.plainTextContent(); } isHtml = true; return html; } static KMime::Types::Mailbox::List stringListToMailboxes(const QStringList &list) { KMime::Types::Mailbox::List mailboxes; for (const auto &s : list) { KMime::Types::Mailbox mb; mb.fromUnicodeString(s); if (mb.hasAddress()) { mailboxes << mb; } else { qWarning() << "Got an invalid address: " << s << list; Q_ASSERT(false); } } return mailboxes; } - static void setRecipients(KMime::Message &message, const Recipients &recipients) { message.to(true)->clear(); const auto tos = stringListToMailboxes(recipients.to); for (const auto &mb : tos) { message.to()->addAddress(mb); } message.cc(true)->clear(); const auto ccs = stringListToMailboxes(recipients.cc); for (const auto &mb : ccs) { message.cc()->addAddress(mb); } message.bcc(true)->clear(); const auto bccs = stringListToMailboxes(recipients.bcc); for (const auto &mb : bccs) { message.bcc()->addAddress(mb); } } - -KMime::Message::Ptr MailTemplates::createIMipMessage( - const QString &from, - const Recipients &recipients, - const QString &subject, - const QString &body, - const QString &attachment) +KMime::Message::Ptr +MailTemplates::createIMipMessage(const QString &from, const Recipients &recipients, const QString &subject, const QString &body, const QString &attachment) { - KMime::Message::Ptr message = KMime::Message::Ptr( new KMime::Message ); - message->contentTransferEncoding()->clear(); // 7Bit, decoded. + KMime::Message::Ptr message = KMime::Message::Ptr(new KMime::Message); + message->contentTransferEncoding()->clear(); // 7Bit, decoded. // Set the headers - message->userAgent()->fromUnicodeString(QStringLiteral("%1/%2(%3)").arg(QString::fromLocal8Bit("GPGOL.js")).arg(u"0.1"_s).arg(QSysInfo::prettyProductName()), "utf-8"); + message->userAgent()->fromUnicodeString( + QStringLiteral("%1/%2(%3)").arg(QString::fromLocal8Bit("GPGOL.js")).arg(u"0.1"_s).arg(QSysInfo::prettyProductName()), + "utf-8"); message->from()->fromUnicodeString(from, "utf-8"); setRecipients(*message, recipients); message->date()->setDateTime(QDateTime::currentDateTime()); message->subject()->fromUnicodeString(subject, "utf-8"); message->contentType()->setMimeType("multipart/alternative"); message->contentType()->setBoundary(KMime::multiPartBoundary()); // Set the first multipart, the body message. KMime::Content *bodyMessage = new KMime::Content{message.data()}; bodyMessage->contentType()->setMimeType("text/plain"); bodyMessage->contentType()->setCharset("utf-8"); bodyMessage->contentTransferEncoding()->setEncoding(KMime::Headers::CEbase64); bodyMessage->setBody(KMime::CRLFtoLF(body.toUtf8())); message->appendContent(bodyMessage); // Set the second multipart, the attachment. KMime::Content *attachMessage = new KMime::Content{message.data()}; attachMessage->contentDisposition()->setDisposition(KMime::Headers::CDattachment); attachMessage->contentType()->setMimeType("text/calendar"); attachMessage->contentType()->setCharset("utf-8"); attachMessage->contentType()->setName(QLatin1String("event.ics"), "utf-8"); attachMessage->contentType()->setParameter(QLatin1String("method"), QLatin1String("REPLY")); attachMessage->contentTransferEncoding()->setEncoding(KMime::Headers::CEbase64); attachMessage->setBody(KMime::CRLFtoLF(attachment.toUtf8())); message->appendContent(attachMessage); // Job done, attach the both multiparts and assemble the message. message->assemble(); return message; } diff --git a/client/editor/mailtemplates.h b/client/editor/mailtemplates.h index 27e12c0..18940d1 100644 --- a/client/editor/mailtemplates.h +++ b/client/editor/mailtemplates.h @@ -1,38 +1,36 @@ // SPDX-FileCopyrightText: 2016 Christian Mollekopf // SPDX-License-Identifier: LGPL-2.0-or-later #pragma once +#include #include #include -#include #include struct Attachment { QString name; QString filename; QByteArray mimeType; bool isInline; QByteArray data; }; struct Recipients { QStringList to; QStringList cc; QStringList bcc; }; namespace MailTemplates { - void reply(const KMime::Message::Ptr &origMsg, const std::function &callback, const KMime::Types::AddrSpecList &me = {}); - void forward(const KMime::Message::Ptr &origMsg, const std::function &callback); - QString plaintextContent(const KMime::Message::Ptr &origMsg); - QString body(const KMime::Message::Ptr &msg, bool &isHtml); +void reply(const KMime::Message::Ptr &origMsg, + const std::function &callback, + const KMime::Types::AddrSpecList &me = {}); +void forward(const KMime::Message::Ptr &origMsg, const std::function &callback); +QString plaintextContent(const KMime::Message::Ptr &origMsg); +QString body(const KMime::Message::Ptr &msg, bool &isHtml); - KMime::Message::Ptr createIMipMessage( - const QString &from, - const Recipients &recipients, - const QString &subject, - const QString &body, - const QString &attachment); +KMime::Message::Ptr +createIMipMessage(const QString &from, const Recipients &recipients, const QString &subject, const QString &body, const QString &attachment); }; diff --git a/client/editor/part/messagepart.h b/client/editor/part/messagepart.h index bb230d8..8a9f554 100644 --- a/client/editor/part/messagepart.h +++ b/client/editor/part/messagepart.h @@ -1,27 +1,26 @@ /* SPDX-FileCopyrightText: 2009 Constantin Berzan SPDX-License-Identifier: LGPL-2.0-or-later */ #pragma once - #include #include namespace MessageComposer { /** * @brief The MessagePart class */ class MessagePart : public QObject { Q_OBJECT public: explicit MessagePart(QObject *parent = nullptr); ~MessagePart() override; }; } diff --git a/client/editor/recipientline.cpp b/client/editor/recipientline.cpp index 2f35943..41812db 100644 --- a/client/editor/recipientline.cpp +++ b/client/editor/recipientline.cpp @@ -1,306 +1,306 @@ /* SPDX-FileCopyrightText: 2010 Casey Link SPDX-FileCopyrightText: 2009-2010 Klaralvdalens Datakonsult AB, a KDAB Group company SPDX-License-Identifier: LGPL-2.0-or-later */ #include "recipientline.h" #include #include #include #include #include #include using namespace KPIM; using namespace Qt::Literals::StringLiterals; RecipientComboBox::RecipientComboBox(QWidget *parent) : QComboBox(parent) { } void RecipientComboBox::keyPressEvent(QKeyEvent *ev) { if (ev->key() == Qt::Key_Right) { Q_EMIT rightPressed(); } else { QComboBox::keyPressEvent(ev); } } RecipientLineEdit::RecipientLineEdit(QWidget *parent) : AddresseeLineEdit(parent) , mToolButton(new QToolButton(this)) { mToolButton->setVisible(false); mToolButton->setCursor(Qt::ArrowCursor); const int size = sizeHint().height() - 5; mToolButton->setFixedSize(size, size); int padding = (sizeHint().height() - size) / 2; mToolButton->move(2, padding); mToolButton->setStyleSheet(QStringLiteral("QToolButton { border: none; }")); connect(mToolButton, &QToolButton::clicked, this, &RecipientLineEdit::iconClicked); } void RecipientLineEdit::keyPressEvent(QKeyEvent *ev) { if (ev->key() == Qt::Key_Left && cursorPosition() == 0 && !ev->modifiers().testFlag(Qt::ShiftModifier)) { // Shift would be pressed during selection Q_EMIT leftPressed(); } else if (ev->key() == Qt::Key_Right && cursorPosition() == text().length() && !ev->modifiers().testFlag(Qt::ShiftModifier)) { // Shift would be pressed during selection Q_EMIT rightPressed(); - } else if ((ev->key() == Qt::Key_Enter || ev->key() == Qt::Key_Return)) {// && !completionBox()->isVisible()) { + } else if ((ev->key() == Qt::Key_Enter || ev->key() == Qt::Key_Return)) { // && !completionBox()->isVisible()) { Q_EMIT focusDown(); KLineEdit::keyPressEvent(ev); return; } else if (ev->key() == Qt::Key_Up) { Q_EMIT focusUp(); return; } else if (ev->key() == Qt::Key_Down) { Q_EMIT focusDown(); return; } else { KLineEdit::keyPressEvent(ev); } } void RecipientLineEdit::setIcon(const QIcon &icon, const QString &tooltip) { if (icon.isNull()) { mToolButton->setVisible(false); setStyleSheet(QString()); } else { mToolButton->setIcon(icon); mToolButton->setToolTip(tooltip); const int padding = mToolButton->width() - style()->pixelMetric(QStyle::PM_DefaultFrameWidth); setStyleSheet(QStringLiteral("QLineEdit { padding-left: %1px }").arg(padding)); mToolButton->setVisible(true); } } RecipientLineNG::RecipientLineNG(QWidget *parent) : MultiplyingLine(parent) , mData(new Recipient) { setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); auto topLayout = new QHBoxLayout(this); topLayout->setContentsMargins({}); const QStringList recipientTypes = Recipient::allTypeLabels(); mCombo = new RecipientComboBox(this); mCombo->addItems(recipientTypes); topLayout->addWidget(mCombo); mCombo->setToolTip(i18nc("@label:listbox", "Select type of recipient")); mEdit = new RecipientLineEdit(this); mEdit->setToolTip(i18n("Set the list of email addresses to receive this message")); mEdit->setClearButtonEnabled(true); topLayout->addWidget(mEdit); mEdit->installEventFilter(this); connect(mEdit, &RecipientLineEdit::returnPressed, this, &RecipientLineNG::slotReturnPressed); connect(mEdit, &RecipientLineEdit::deleteMe, this, &RecipientLineNG::slotPropagateDeletion); connect(mEdit, &QLineEdit::textChanged, this, &RecipientLineNG::analyzeLine); connect(mEdit, &RecipientLineEdit::focusUp, this, &RecipientLineNG::slotFocusUp); connect(mEdit, &RecipientLineEdit::focusDown, this, &RecipientLineNG::slotFocusDown); connect(mEdit, &RecipientLineEdit::rightPressed, this, &RecipientLineNG::rightPressed); connect(mEdit, &RecipientLineEdit::iconClicked, this, &RecipientLineNG::iconClicked); connect(mEdit, &RecipientLineEdit::leftPressed, mCombo, qOverload<>(&QWidget::setFocus)); connect(mEdit, &RecipientLineEdit::editingFinished, this, &RecipientLineNG::slotEditingFinished); connect(mEdit, &RecipientLineEdit::clearButtonClicked, this, &RecipientLineNG::slotPropagateDeletion); connect(mCombo, &RecipientComboBox::rightPressed, mEdit, qOverload<>(&QWidget::setFocus)); connect(mCombo, &RecipientComboBox::activated, this, &RecipientLineNG::slotTypeModified); connect(mEdit, &RecipientLineEdit::addAddress, this, &RecipientLineNG::slotAddRecipient); } void RecipientLineNG::slotEditingFinished() { if (mEdit->text().isEmpty()) { Q_EMIT deleteLine(this); } } void RecipientLineNG::slotAddRecipient(const QString &email) { Q_EMIT addRecipient(this, email); slotReturnPressed(); } void RecipientLineNG::slotTypeModified() { mModified = true; Q_EMIT typeModified(this); } void RecipientLineNG::analyzeLine(const QString &text) { const QStringList r = KEmailAddress::splitAddressList(text); mRecipientsCount = r.count(); mModified = true; Q_EMIT countChanged(); } int RecipientLineNG::recipientsCount() const { return mRecipientsCount; } void RecipientLineNG::setData(const MultiplyingLineData::Ptr &data) { Recipient::Ptr rec = qSharedPointerDynamicCast(data); if (rec.isNull()) { return; } mData = rec; fieldsFromData(); } MultiplyingLineData::Ptr RecipientLineNG::data() const { if (isModified()) { const_cast(this)->dataFromFields(); } return mData; } void RecipientLineNG::dataFromFields() { if (!mData) { return; } const QString editStr(mEdit->text()); QString displayName; QString addrSpec; QString comment; if (KEmailAddress::splitAddress(editStr, displayName, addrSpec, comment) == KEmailAddress::AddressOk) { mData->setName(displayName); } KMime::Types::Mailbox mbox; mbox.from7BitString(editStr.toUtf8()); if (mbox.hasAddress()) { mData->setEmail(mbox.addrSpec().asString()); mData->setName(mbox.name()); } else { mData->setEmail(editStr); } mData->setType(Recipient::idToType(mCombo->currentIndex())); mModified = false; } void RecipientLineNG::fieldsFromData() { if (!mData) { return; } mCombo->setCurrentIndex(Recipient::typeToId(mData->type())); - mEdit->setText(mData->name().isEmpty() ? mData->email() : mData->name() + u" <"_s +mData->email() + u'>'); + mEdit->setText(mData->name().isEmpty() ? mData->email() : mData->name() + u" <"_s + mData->email() + u'>'); } void RecipientLineNG::activate() { mEdit->setFocus(); } bool RecipientLineNG::isActive() const { return mEdit->hasFocus(); } bool RecipientLineNG::isEmpty() const { return mEdit->text().isEmpty(); } bool RecipientLineNG::isModified() const { return mModified || mEdit->isModified(); } void RecipientLineNG::clearModified() { mModified = false; mEdit->setModified(false); } int RecipientLineNG::setColumnWidth(int w) { w = qMax(w, mCombo->sizeHint().width()); mCombo->setFixedWidth(w); mCombo->updateGeometry(); parentWidget()->updateGeometry(); return w; } void RecipientLineNG::fixTabOrder(QWidget *previous) { setTabOrder(previous, mCombo); setTabOrder(mCombo, mEdit); } QWidget *RecipientLineNG::tabOut() const { return mEdit; } void RecipientLineNG::clear() { mRecipientsCount = 0; mEdit->clear(); } bool RecipientLineNG::canDeleteLineEdit() const { return true; } void RecipientLineNG::setCompletionMode(KCompletion::CompletionMode mode) { mEdit->setCompletionMode(mode); } Recipient::Type RecipientLineNG::recipientType() const { return Recipient::idToType(mCombo->currentIndex()); } void RecipientLineNG::setRecipientType(Recipient::Type type) { mCombo->setCurrentIndex(Recipient::typeToId(type)); slotTypeModified(); } Recipient::Ptr RecipientLineNG::recipient() const { return qSharedPointerDynamicCast(data()); } void RecipientLineNG::setIcon(const QIcon &icon, const QString &tooltip) { mEdit->setIcon(icon, tooltip); } QString RecipientLineNG::rawData() const { return mEdit->text(); } bool RecipientLineNG::eventFilter(QObject *watched, QEvent *event) { if (watched == mEdit) { if (event->type() == QEvent::FocusIn || event->type() == QEvent::FocusOut) { Q_EMIT activeChanged(); } } return false; } diff --git a/client/editor/recipientline.h b/client/editor/recipientline.h index a347aff..37a5b3e 100644 --- a/client/editor/recipientline.h +++ b/client/editor/recipientline.h @@ -1,127 +1,127 @@ /* SPDX-FileCopyrightText: 2010 Casey Link SPDX-FileCopyrightText: 2009-2010 Klaralvdalens Datakonsult AB, a KDAB Group company SPDX-License-Identifier: LGPL-2.0-or-later */ #pragma once #include -#include #include +#include #include -#include "recipient.h" #include "addresseelineedit.h" +#include "recipient.h" class KConfig; class RecipientComboBox : public QComboBox { Q_OBJECT public: explicit RecipientComboBox(QWidget *parent); Q_SIGNALS: void rightPressed(); protected: void keyPressEvent(QKeyEvent *ev) override; }; /** * @brief The RecipientLineEdit class */ class RecipientLineEdit : public AddresseeLineEdit { Q_OBJECT public: explicit RecipientLineEdit(QWidget *parent); void setIcon(const QIcon &icon, const QString &tooltip); Q_SIGNALS: void addAddress(const QString &address); void iconClicked(); void focusUp(); void focusDown(); void deleteMe(); void leftPressed(); void rightPressed(); protected: void keyPressEvent(QKeyEvent *ev) override; private: QToolButton *const mToolButton; }; /** * @brief The RecipientLineNG class */ class RecipientLineNG : public KPIM::MultiplyingLine { Q_OBJECT public: explicit RecipientLineNG(QWidget *parent); ~RecipientLineNG() override = default; void activate() override; [[nodiscard]] bool isActive() const override; [[nodiscard]] bool isEmpty() const override; void clear() override; [[nodiscard]] bool canDeleteLineEdit() const override; [[nodiscard]] bool isModified() const override; void clearModified() override; [[nodiscard]] KPIM::MultiplyingLineData::Ptr data() const override; void setData(const KPIM::MultiplyingLineData::Ptr &data) override; void fixTabOrder(QWidget *previous) override; [[nodiscard]] QWidget *tabOut() const override; void setCompletionMode(KCompletion::CompletionMode mode) override; [[nodiscard]] int setColumnWidth(int w) override; // recipient specific methods [[nodiscard]] int recipientsCount() const; void setRecipientType(Recipient::Type); [[nodiscard]] Recipient::Type recipientType() const; [[nodiscard]] QSharedPointer recipient() const; void setIcon(const QIcon &icon, const QString &tooltip = QString()); [[nodiscard]] QString rawData() const; Q_SIGNALS: void typeModified(RecipientLineNG *); void addRecipient(RecipientLineNG *, const QString &); void countChanged(); void iconClicked(); void activeChanged(); protected Q_SLOTS: void slotEditingFinished(); void slotTypeModified(); void analyzeLine(const QString &); protected: bool eventFilter(QObject *watched, QEvent *event) override; private: void dataFromFields(); void fieldsFromData(); void slotAddRecipient(const QString &); RecipientComboBox *mCombo = nullptr; RecipientLineEdit *mEdit = nullptr; int mRecipientsCount = 0; bool mModified = false; QSharedPointer mData; }; diff --git a/client/editor/recipientseditor.cpp b/client/editor/recipientseditor.cpp index d418efa..e350845 100644 --- a/client/editor/recipientseditor.cpp +++ b/client/editor/recipientseditor.cpp @@ -1,310 +1,308 @@ /* SPDX-FileCopyrightText: 2010 Casey Link SPDX-FileCopyrightText: 2009-2010 Klaralvdalens Datakonsult AB, a KDAB Group company Refactored from earlier code by: SPDX-FileCopyrightText: 2010 Volker Krause SPDX-FileCopyrightText: 2004 Cornelius Schumacher SPDX-License-Identifier: LGPL-2.0-or-later */ #include "recipientseditor.h" #include "recipient.h" #include #include #include #include +#include #include #include -#include using namespace KPIM; constexpr int gMaximumRecipients = 100; RecipientLineFactory::RecipientLineFactory(QObject *parent) : KPIM::MultiplyingLineFactory(parent) { } KPIM::MultiplyingLine *RecipientLineFactory::newLine(QWidget *p) { auto line = new RecipientLineNG(p); if (qobject_cast(parent())) { - connect(line, &RecipientLineNG::addRecipient, - qobject_cast(parent()), - &RecipientsEditor::addRecipientSlot); + connect(line, &RecipientLineNG::addRecipient, qobject_cast(parent()), &RecipientsEditor::addRecipientSlot); } else { qWarning() << "RecipientLineFactory::newLine: We can't connect to new line" << parent(); } return line; } int RecipientLineFactory::maximumRecipients() { return gMaximumRecipients; } class RecipientsEditorPrivate { public: RecipientsEditorPrivate() = default; KConfig *mRecentAddressConfig = nullptr; bool mSkipTotal = false; }; RecipientsEditor::RecipientsEditor(QWidget *parent) : RecipientsEditor(new RecipientLineFactory(nullptr), parent) { } RecipientsEditor::RecipientsEditor(RecipientLineFactory *lineFactory, QWidget *parent) : MultiplyingLineEditor(lineFactory, parent) , d(new RecipientsEditorPrivate) { factory()->setParent(this); // HACK: can't use 'this' above since it's not yet constructed at that point // 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(this, &RecipientsEditor::lineAdded, this, &RecipientsEditor::slotLineAdded); connect(this, &RecipientsEditor::lineDeleted, this, &RecipientsEditor::slotLineDeleted); addData(); // one default line } RecipientsEditor::~RecipientsEditor() = default; bool RecipientsEditor::addRecipient(const QString &recipient, Recipient::Type type) { return addData(Recipient::Ptr(new Recipient(recipient, type)), false); } void RecipientsEditor::addRecipientSlot(RecipientLineNG *line, const QString &recipient) { addRecipient(recipient, line->recipientType()); } bool RecipientsEditor::setRecipientString(const QList &mailboxes, Recipient::Type type) { int count = 1; for (const KMime::Types::Mailbox &mailbox : mailboxes) { if (count++ > gMaximumRecipients) { KMessageBox::error(this, i18ncp("@info:status", "Truncating recipients list to %2 of %1 entry.", "Truncating recipients list to %2 of %1 entries.", mailboxes.count(), gMaximumRecipients)); return true; } // Too many if (addRecipient(mailbox.prettyAddress(KMime::Types::Mailbox::QuoteWhenNecessary), type)) { return true; } } return false; } 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.isNull()) { 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) { if (r->name().isEmpty()) { selectedRecipients << r->email(); } else { selectedRecipients << r->name() + u" <" + r->email() + u'>'; } } } 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(); auto rec = qobject_cast(line); if (rec) { if ((rec->recipient()->email() == recipient) && (rec->recipientType() == type)) { break; } } } if (line) { line->slotPropagateDeletion(); } } void RecipientsEditor::slotLineAdded(MultiplyingLine *line) { // subtract 1 here, because we want the number of lines // before this line was added. const int count = lines().size() - 1; auto rec = qobject_cast(line); if (!rec) { return; } if (count > 0) { if (count == 1) { rec->setRecipientType(Recipient::To); } else { auto last_rec = qobject_cast(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); auto rec = qobject_cast(line); if (rec) { if (rec->recipientType() == Recipient::To) { atLeastOneToLine = true; } else if ((rec->recipientType() == Recipient::Cc) && (firstCC < 0)) { firstCC = i; } } } if (!atLeastOneToLine && (firstCC >= 0)) { auto firstCCLine = qobject_cast(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)) { 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; } } } else if (event->type() == QEvent::FocusIn && qobject_cast(object)) { Q_EMIT focusInRecipientLineEdit(); } return false; } void RecipientsEditor::slotCalculateTotal() { // Prevent endless recursion when splitting recipient if (d->mSkipTotal) { return; } int empty = 0; const auto currentLines = lines(); for (auto line : currentLines) { auto rec = qobject_cast(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->setRawEmail(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({}, false); } int count = 0; const auto linesP{lines()}; for (auto line : linesP) { auto rec = qobject_cast(line); if (rec) { if (!rec->isEmpty()) { count++; } } } } RecipientLineNG *RecipientsEditor::activeLine() const { MultiplyingLine *line = MultiplyingLineEditor::activeLine(); return qobject_cast(line); } diff --git a/client/editor/recipientseditor.h b/client/editor/recipientseditor.h index cc16693..a398e19 100644 --- a/client/editor/recipientseditor.h +++ b/client/editor/recipientseditor.h @@ -1,90 +1,90 @@ // SPDX-FileCopyrightText: 2010 Casey Link // SPDX-FileCopyrightText: 2009-2010 Klaralvdalens Datakonsult AB, a KDAB Group company // // Refactored from earlier code by: // SPDX-FileCopyrightText: 2010 Volker Krause // SPDX-FileCopyrightText: 2004 Cornelius Schumacher // // SPDX-License-Identifier: LGPL-2.0-or-later #pragma once -#include "recipientline.h" #include "recipient.h" +#include "recipientline.h" #include namespace KMime { namespace Types { class Mailbox; } } /** * @brief The RecipientLineFactory class */ class RecipientLineFactory : public KPIM::MultiplyingLineFactory { Q_OBJECT public: explicit RecipientLineFactory(QObject *parent); KPIM::MultiplyingLine *newLine(QWidget *parent) override; int maximumRecipients() override; }; class RecipientsEditorPrivate; /** * @brief The RecipientsEditor class */ class RecipientsEditor : public KPIM::MultiplyingLineEditor { Q_OBJECT public: explicit RecipientsEditor(QWidget *parent = nullptr); explicit RecipientsEditor(RecipientLineFactory *lineFactory, QWidget *parent = nullptr); ~RecipientsEditor() override; [[nodiscard]] Recipient::List recipients() const; [[nodiscard]] QSharedPointer activeRecipient() const; bool setRecipientString(const QList &mailboxes, Recipient::Type); [[nodiscard]] QString recipientString(Recipient::Type) const; [[nodiscard]] QStringList recipientStringList(Recipient::Type) const; /** Adds a recipient (or multiple recipients) to one line of the editor. @param recipient The recipient(s) you want to add. @param type The recipient type. */ bool addRecipient(const QString &recipient, Recipient::Type type); /** Removes the recipient provided it can be found and has the given type. @param recipient The recipient(s) you want to remove. @param type The recipient type. */ void removeRecipient(const QString &recipient, Recipient::Type type); /** * Sets the config file used for storing recent addresses. */ void setRecentAddressConfig(KConfig *config); public Q_SLOTS: void addRecipientSlot(RecipientLineNG *, const QString &); protected Q_SLOTS: void slotLineAdded(KPIM::MultiplyingLine *); void slotLineDeleted(int pos); void slotCalculateTotal(); protected: bool eventFilter(QObject *object, QEvent *event) override; [[nodiscard]] RecipientLineNG *activeLine() const override; Q_SIGNALS: void focusInRecipientLineEdit(); private: std::unique_ptr const d; }; diff --git a/client/editor/richtextcomposerng.cpp b/client/editor/richtextcomposerng.cpp index 664692a..72ca9f6 100644 --- a/client/editor/richtextcomposerng.cpp +++ b/client/editor/richtextcomposerng.cpp @@ -1,431 +1,431 @@ /* SPDX-FileCopyrightText: 2015-2023 Laurent Montel SPDX-License-Identifier: GPL-2.0-or-later */ #include "richtextcomposerng.h" -#include "richtextcomposersignatures.h" #include "messagecomposersettings.h" +#include "richtextcomposersignatures.h" #include #include #include #include #include #include #include "part/textpart.h" #include #include #define USE_TEXTHTML_BUILDER 1 using namespace MessageComposer; class MessageComposer::RichTextComposerNgPrivate { public: explicit RichTextComposerNgPrivate(RichTextComposerNg *q) : richtextComposer(q) , richTextComposerSignatures(new MessageComposer::RichTextComposerSignatures(richtextComposer, richtextComposer)) { } void fixHtmlFontSize(QString &cleanHtml) const; [[nodiscard]] QString toCleanHtml() const; TextAutoCorrectionCore::AutoCorrection *autoCorrection = nullptr; RichTextComposerNg *const richtextComposer; MessageComposer::RichTextComposerSignatures *const richTextComposerSignatures; }; RichTextComposerNg::RichTextComposerNg(QWidget *parent) : KPIMTextEdit::RichTextComposer(parent) , d(new MessageComposer::RichTextComposerNgPrivate(this)) { } RichTextComposerNg::~RichTextComposerNg() = default; MessageComposer::RichTextComposerSignatures *RichTextComposerNg::composerSignature() const { return d->richTextComposerSignatures; } TextAutoCorrectionCore::AutoCorrection *RichTextComposerNg::autocorrection() const { return d->autoCorrection; } void RichTextComposerNg::setAutocorrection(TextAutoCorrectionCore::AutoCorrection *autocorrect) { d->autoCorrection = autocorrect; } void RichTextComposerNg::setAutocorrectionLanguage(const QString &lang) { if (d->autoCorrection) { TextAutoCorrectionCore::AutoCorrectionSettings *settings = d->autoCorrection->autoCorrectionSettings(); settings->setLanguage(lang); d->autoCorrection->setAutoCorrectionSettings(settings); } } 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->autoCorrectionSettings()->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) const { // non-greedy matching static const QRegularExpression styleRegex(QStringLiteral("")); QRegularExpressionMatch rmatch; int offset = 0; while (cleanHtml.indexOf(styleRegex, offset, &rmatch) != -1) { QString replacement; bool ok = false; const double ptValue = rmatch.captured(1).toDouble(&ok); if (ok) { const double emValue = ptValue / 12; replacement = QString::number(emValue, 'g', 2); const int capLen = rmatch.capturedLength(1); cleanHtml.replace(rmatch.capturedStart(1), capLen + 2 /* QLatin1String("pt").size() */, replacement + QLatin1String("em")); // advance the offset to just after the last replace offset = rmatch.capturedEnd(0) - capLen + replacement.size(); } else { // a match was found but the toDouble call failed, advance the offset to just after // the entire match offset = rmatch.capturedEnd(0); } } } void RichTextComposerNg::fillComposerTextPart(MessageComposer::TextPart *textPart) { const bool wasConverted = false; if (composerControler()->isFormattingUsed()) { if (!wasConverted) { if (MessageComposer::MessageComposerSettings::self()->improvePlainTextOfHtmlMessage()) { auto pb = new KPIMTextEdit::PlainTextMarkupBuilder(); auto pmd = new KPIMTextEdit::MarkupDirector(pb); pmd->processDocument(document()); const QString plainText = pb->getResult(); textPart->setCleanPlainText(composerControler()->toCleanPlainText(plainText)); auto 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 auto pb = new KPIMTextEdit::TextHTMLBuilder(); auto 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; // qDebug() << " d->toCleanHtml() " << d->toCleanHtml(); #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'. // Minimal/non-greedy matching static const QString EMPTYLINEREGEX = QStringLiteral("

"); static const QString OLLISTPATTERNQT = QStringLiteral("

    elements with

     

    . // Replace all the matching text with the new line text result.replace(QRegularExpression(EMPTYLINEREGEX), EMPTYLINEHTML); // 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, KIdentityManagementCore::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 == KIdentityManagementCore::Signature::End) { cursor.movePosition(QTextCursor::End); } else if (placement == KIdentityManagementCore::Signature::Start) { cursor.movePosition(QTextCursor::Start); } else if (placement == KIdentityManagementCore::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 == KIdentityManagementCore::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 == KIdentityManagementCore::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 == KIdentityManagementCore::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 KIdentityManagementCore::Signature &signature, KIdentityManagementCore::Signature::Placement placement, KIdentityManagementCore::Signature::AddedText addedText) { if (signature.isEnabledSignature()) { QString signatureStr; bool ok = false; QString errorMessage; if (addedText & KIdentityManagementCore::Signature::AddSeparator) { signatureStr = signature.withSeparator(&ok, &errorMessage); } else { signatureStr = signature.rawText(&ok, &errorMessage); } if (!ok && !errorMessage.isEmpty()) { KMessageBox::error(nullptr, errorMessage); } insertSignatureHelper(signatureStr, this, placement, (signature.isInlinedHtml() && signature.type() == KIdentityManagementCore::Signature::Inlined), (addedText & KIdentityManagementCore::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(); for (const KIdentityManagementCore::Signature::EmbeddedImagePtr &image : embeddedImages) { composerControler()->composerImages()->loadImage(image->image, image->name, image->name); } } } } QString RichTextComposerNg::toCleanHtml() const { return d->toCleanHtml(); } void RichTextComposerNg::fixHtmlFontSize(QString &cleanHtml) const { d->fixHtmlFontSize(cleanHtml); } void RichTextComposerNg::forceAutoCorrection(bool selectedText) { if (document()->isEmpty()) { return; } if (d->autoCorrection && d->autoCorrection->autoCorrectionSettings()->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); } } } #include "moc_richtextcomposerng.cpp" diff --git a/client/editor/signaturecontroller.cpp b/client/editor/signaturecontroller.cpp index 15dc49c..408eb50 100644 --- a/client/editor/signaturecontroller.cpp +++ b/client/editor/signaturecontroller.cpp @@ -1,117 +1,117 @@ /* * SPDX-FileCopyrightText: 2010 Volker Krause * * Based on kmail/kmcomposewin.cpp * SPDX-FileCopyrightText: 2009 Constantin Berzan * * Based on KMail code by: * SPDX-FileCopyrightText: 1997 Markus Wuebben * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "signaturecontroller.h" +#include "messagecomposersettings.h" #include "richtextcomposerng.h" #include "richtextcomposersignatures.h" -#include "messagecomposersettings.h" #include "identity/identity.h" using namespace MessageComposer; class MessageComposer::SignatureControllerPrivate { public: SignatureControllerPrivate() = default; KIdentityManagementCore::Identity m_currentIdentity; KIdentityManagementWidgets::IdentityCombo *m_identityCombo = nullptr; MessageComposer::RichTextComposerNg *m_editor = nullptr; }; SignatureController::SignatureController(QObject *parent) : QObject(parent) , d(new MessageComposer::SignatureControllerPrivate) { } SignatureController::~SignatureController() = default; void SignatureController::setEditor(MessageComposer::RichTextComposerNg *editor) { d->m_editor = editor; } void SignatureController::setIdentity(const KIdentityManagementCore::Identity &identity) { d->m_currentIdentity = identity; } void SignatureController::appendSignature() { insertSignatureHelper(KIdentityManagementCore::Signature::End); } void SignatureController::prependSignature() { insertSignatureHelper(KIdentityManagementCore::Signature::Start); } void SignatureController::insertSignatureAtCursor() { insertSignatureHelper(KIdentityManagementCore::Signature::AtCursor); } void SignatureController::cleanSpace() { if (!d->m_editor || d->m_currentIdentity.isNull()) { return; } const KIdentityManagementCore::Signature signature = d->m_currentIdentity.signature(); d->m_editor->composerSignature()->cleanWhitespace(signature); } void SignatureController::insertSignatureHelper(KIdentityManagementCore::Signature::Placement placement) { if (!d->m_editor || d->m_currentIdentity.isNull()) { return; } // Identity::signature() is not const, although it should be, therefore the // const_cast. const KIdentityManagementCore::Signature signature = d->m_currentIdentity.signature(); if (signature.isInlinedHtml() && signature.type() == KIdentityManagementCore::Signature::Inlined) { Q_EMIT enableHtml(); } KIdentityManagementCore::Signature::AddedText addedText = KIdentityManagementCore::Signature::AddNewLines; if (MessageComposer::MessageComposerSettings::self()->dashDashSignature()) { addedText |= KIdentityManagementCore::Signature::AddSeparator; } d->m_editor->insertSignature(signature, placement, addedText); if ((placement == KIdentityManagementCore::Signature::Start) || (placement == KIdentityManagementCore::Signature::End)) { Q_EMIT signatureAdded(); } } void SignatureController::applySignature(const KIdentityManagementCore::Signature &signature) { if (!d->m_editor) { return; } if (MessageComposer::MessageComposerSettings::self()->autoTextSignature() == QLatin1String("auto")) { KIdentityManagementCore::Signature::AddedText addedText = KIdentityManagementCore::Signature::AddNewLines; if (MessageComposer::MessageComposerSettings::self()->dashDashSignature()) { addedText |= KIdentityManagementCore::Signature::AddSeparator; } if (MessageComposer::MessageComposerSettings::self()->prependSignature()) { d->m_editor->insertSignature(signature, KIdentityManagementCore::Signature::Start, addedText); } else { d->m_editor->insertSignature(signature, KIdentityManagementCore::Signature::End, addedText); } } } #include "moc_signaturecontroller.cpp" diff --git a/client/editor/urlrequester.h b/client/editor/urlrequester.h index fd04914..9e8a0f9 100644 --- a/client/editor/urlrequester.h +++ b/client/editor/urlrequester.h @@ -1,686 +1,686 @@ /* This file is part of the KDE libraries SPDX-FileCopyrightText: 1999, 2000, 2001 Carsten Pfeiffer SPDX-FileCopyrightText: 2013 Teo Mrnjavac SPDX-License-Identifier: LGPL-2.0-only */ #include "urlrequester.h" #include #include #include #include #include #include #include #include #include #include #include #include #include class KUrlDragPushButton : public QPushButton { Q_OBJECT public: explicit KUrlDragPushButton(QWidget *parent) : QPushButton(parent) { new DragDecorator(this); } ~KUrlDragPushButton() override { } void setURL(const QUrl &url) { m_urls.clear(); m_urls.append(url); } private: class DragDecorator : public KDragWidgetDecoratorBase { public: explicit DragDecorator(KUrlDragPushButton *button) : KDragWidgetDecoratorBase(button) , m_button(button) { } protected: QDrag *dragObject() override { if (m_button->m_urls.isEmpty()) { return nullptr; } QDrag *drag = new QDrag(m_button); QMimeData *mimeData = new QMimeData; mimeData->setUrls(m_button->m_urls); drag->setMimeData(mimeData); return drag; } private: KUrlDragPushButton *m_button; }; QList m_urls; }; class KUrlRequester::KUrlRequesterPrivate { public: explicit KUrlRequesterPrivate(KUrlRequester *parent) : m_fileDialogModeWasDirAndFile(false) , m_parent(parent) , edit(nullptr) , combo(nullptr) , fileDialogMode(KFile::File | KFile::ExistingOnly | KFile::LocalOnly) , fileDialogAcceptMode(QFileDialog::AcceptOpen) { } ~KUrlRequesterPrivate() { delete myCompletion; delete myFileDialog; } void init(); void setText(const QString &text) { if (combo) { if (combo->isEditable()) { combo->setEditText(text); } else { int i = combo->findText(text); if (i == -1) { combo->addItem(text); combo->setCurrentIndex(combo->count() - 1); } else { combo->setCurrentIndex(i); } } } else { edit->setText(text); } } void connectSignals(KUrlRequester *receiver) { if (combo) { connect(combo, &QComboBox::currentTextChanged, receiver, &KUrlRequester::textChanged); connect(combo, &QComboBox::editTextChanged, receiver, &KUrlRequester::textEdited); connect(combo, &KComboBox::returnPressed, receiver, &KUrlRequester::returnPressed); } else if (edit) { connect(edit, &QLineEdit::textChanged, receiver, &KUrlRequester::textChanged); connect(edit, &QLineEdit::textEdited, receiver, &KUrlRequester::textEdited); connect(edit, qOverload<>(&QLineEdit::returnPressed), receiver, [this]() { m_parent->Q_EMIT returnPressed(QString{}); }); if (auto kline = qobject_cast(edit)) { connect(kline, &KLineEdit::returnKeyPressed, receiver, &KUrlRequester::returnPressed); } } } void setCompletionObject(KCompletion *comp) { if (combo) { combo->setCompletionObject(comp); } else { edit->setCompletionObject(comp); } } void updateCompletionStartDir(const QUrl &newStartDir) { myCompletion->setDir(newStartDir); } QString text() const { return combo ? combo->currentText() : edit->text(); } /** * replaces ~user or $FOO, if necessary * if text() is a relative path, make it absolute using startDir() */ QUrl url() const { const QString txt = text(); KUrlCompletion *comp; if (combo) { comp = qobject_cast(combo->completionObject()); } else { comp = qobject_cast(edit->completionObject()); } QString enteredPath; if (comp) { enteredPath = comp->replacedPath(txt); } else { enteredPath = txt; } if (Utils::isAbsoluteLocalPath(enteredPath)) { return QUrl::fromLocalFile(enteredPath); } const QUrl enteredUrl = QUrl(enteredPath); // absolute or relative if (enteredUrl.isRelative() && !txt.isEmpty()) { QUrl finalUrl(m_startDir); finalUrl.setPath(Utils::concatPaths(finalUrl.path(), enteredPath)); return finalUrl; } else { return enteredUrl; } } static void applyFileMode(QFileDialog *dlg, KFile::Modes m, QFileDialog::AcceptMode acceptMode) { QFileDialog::FileMode fileMode; bool dirsOnly = false; if (m & KFile::Directory) { fileMode = QFileDialog::Directory; if ((m & KFile::File) == 0 && (m & KFile::Files) == 0) { dirsOnly = true; } } else if (m & KFile::Files && m & KFile::ExistingOnly) { fileMode = QFileDialog::ExistingFiles; } else if (m & KFile::File && m & KFile::ExistingOnly) { fileMode = QFileDialog::ExistingFile; } else { fileMode = QFileDialog::AnyFile; } dlg->setFileMode(fileMode); dlg->setAcceptMode(acceptMode); dlg->setOption(QFileDialog::ShowDirsOnly, dirsOnly); } QUrl getDirFromFileDialog(const QUrl &openUrl) const { return QFileDialog::getExistingDirectoryUrl(m_parent, QString(), openUrl, QFileDialog::ShowDirsOnly); } void createFileDialog() { // Creates the fileDialog if it doesn't exist yet QFileDialog *dlg = m_parent->fileDialog(); if (!url().isEmpty() && !url().isRelative()) { QUrl u(url()); // If we won't be able to list it (e.g. http), then don't try :) if (KProtocolManager::supportsListing(u)) { dlg->selectUrl(u); } } else { dlg->setDirectoryUrl(m_startDir); } dlg->setAcceptMode(fileDialogAcceptMode); // Update the file dialog window modality if (dlg->windowModality() != fileDialogModality) { dlg->setWindowModality(fileDialogModality); } if (fileDialogModality == Qt::NonModal) { dlg->show(); } else { dlg->exec(); } } // slots void slotUpdateUrl(); void slotOpenDialog(); void slotFileDialogAccepted(); QUrl m_startDir; bool m_startDirCustomized; bool m_fileDialogModeWasDirAndFile; KUrlRequester *const m_parent; // TODO: rename to 'q' KLineEdit *edit; KComboBox *combo; KFile::Modes fileDialogMode; QFileDialog::AcceptMode fileDialogAcceptMode; QStringList nameFilters; QStringList mimeTypeFilters; KEditListWidget::CustomEditor editor; KUrlDragPushButton *myButton; QFileDialog *myFileDialog; KUrlCompletion *myCompletion; Qt::WindowModality fileDialogModality; }; KUrlRequester::KUrlRequester(QWidget *editWidget, QWidget *parent) : QWidget(parent) , d(new KUrlRequesterPrivate(this)) { // must have this as parent editWidget->setParent(this); d->combo = qobject_cast(editWidget); d->edit = qobject_cast(editWidget); if (d->edit) { d->edit->setClearButtonEnabled(true); } d->init(); } KUrlRequester::KUrlRequester(QWidget *parent) : QWidget(parent) , d(new KUrlRequesterPrivate(this)) { d->init(); } KUrlRequester::KUrlRequester(const QUrl &url, QWidget *parent) : QWidget(parent) , d(new KUrlRequesterPrivate(this)) { d->init(); setUrl(url); } KUrlRequester::~KUrlRequester() { QWidget *widget = d->combo ? static_cast(d->combo) : static_cast(d->edit); widget->removeEventFilter(this); } void KUrlRequester::KUrlRequesterPrivate::init() { myFileDialog = nullptr; fileDialogModality = Qt::ApplicationModal; if (!combo && !edit) { edit = new KLineEdit(m_parent); edit->setClearButtonEnabled(true); } QWidget *widget = combo ? static_cast(combo) : static_cast(edit); QHBoxLayout *topLayout = new QHBoxLayout(m_parent); topLayout->setContentsMargins(0, 0, 0, 0); topLayout->setSpacing(-1); // use default spacing topLayout->addWidget(widget); myButton = new KUrlDragPushButton(m_parent); myButton->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); int buttonSize = myButton->sizeHint().expandedTo(widget->sizeHint()).height(); myButton->setFixedSize(buttonSize, buttonSize); myButton->setToolTip(i18n("Open file dialog")); connect(myButton, &KUrlDragPushButton::pressed, m_parent, [this]() { slotUpdateUrl(); }); widget->installEventFilter(m_parent); m_parent->setFocusProxy(widget); m_parent->setFocusPolicy(Qt::StrongFocus); topLayout->addWidget(myButton); connectSignals(m_parent); connect(myButton, &KUrlDragPushButton::clicked, m_parent, [this]() { slotOpenDialog(); }); m_startDir = QUrl::fromLocalFile(QDir::currentPath()); m_startDirCustomized = false; myCompletion = new KUrlCompletion(); updateCompletionStartDir(m_startDir); setCompletionObject(myCompletion); QAction *openAction = new QAction(m_parent); openAction->setShortcut(QKeySequence::Open); m_parent->connect(openAction, &QAction::triggered, m_parent, [this]() { slotOpenDialog(); }); } void KUrlRequester::setUrl(const QUrl &url) { d->setText(url.toDisplayString(QUrl::PreferLocalFile)); } void KUrlRequester::setText(const QString &text) { d->setText(text); } void KUrlRequester::setStartDir(const QUrl &startDir) { d->m_startDir = startDir; d->m_startDirCustomized = true; d->updateCompletionStartDir(startDir); } void KUrlRequester::changeEvent(QEvent *e) { if (e->type() == QEvent::WindowTitleChange) { if (d->myFileDialog) { d->myFileDialog->setWindowTitle(windowTitle()); } } QWidget::changeEvent(e); } QUrl KUrlRequester::url() const { - #include "urlrequester.moc" return d->url(); +#include "urlrequester.moc" return d->url(); } QUrl KUrlRequester::startDir() const { return d->m_startDir; } QString KUrlRequester::text() const { return d->text(); } void KUrlRequester::KUrlRequesterPrivate::slotOpenDialog() { if (myFileDialog) { if (myFileDialog->isVisible()) { // The file dialog is already being shown, raise it and exit myFileDialog->raise(); myFileDialog->activateWindow(); return; } } if (!m_fileDialogModeWasDirAndFile && (((fileDialogMode & KFile::Directory) && !(fileDialogMode & KFile::File)) || /* catch possible fileDialog()->setMode( KFile::Directory ) changes */ (myFileDialog && (myFileDialog->fileMode() == QFileDialog::Directory && myFileDialog->testOption(QFileDialog::ShowDirsOnly))))) { const QUrl openUrl = (!m_parent->url().isEmpty() && !m_parent->url().isRelative()) ? m_parent->url() : m_startDir; /* FIXME We need a new abstract interface for using KDirSelectDialog in a non-modal way */ QUrl newUrl; if (fileDialogMode & KFile::LocalOnly) { newUrl = QFileDialog::getExistingDirectoryUrl(m_parent, QString(), openUrl, QFileDialog::ShowDirsOnly, QStringList() << QStringLiteral("file")); } else { newUrl = getDirFromFileDialog(openUrl); } if (newUrl.isValid()) { m_parent->setUrl(newUrl); Q_EMIT m_parent->urlSelected(url()); } } else { Q_EMIT m_parent->openFileDialog(m_parent); if (((fileDialogMode & KFile::Directory) && (fileDialogMode & KFile::File)) || m_fileDialogModeWasDirAndFile) { QMenu *dirOrFileMenu = new QMenu(); QAction *fileAction = new QAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("File")); QAction *dirAction = new QAction(QIcon::fromTheme(QStringLiteral("folder-new")), i18n("Directory")); dirOrFileMenu->addAction(fileAction); dirOrFileMenu->addAction(dirAction); connect(fileAction, &QAction::triggered, [this]() { fileDialogMode = KFile::File; applyFileMode(m_parent->fileDialog(), fileDialogMode, fileDialogAcceptMode); m_fileDialogModeWasDirAndFile = true; createFileDialog(); }); connect(dirAction, &QAction::triggered, [this]() { fileDialogMode = KFile::Directory; applyFileMode(m_parent->fileDialog(), fileDialogMode, fileDialogAcceptMode); m_fileDialogModeWasDirAndFile = true; createFileDialog(); }); dirOrFileMenu->exec(m_parent->mapToGlobal(QPoint(m_parent->width(), m_parent->height()))); return; } createFileDialog(); } } void KUrlRequester::KUrlRequesterPrivate::slotFileDialogAccepted() { if (!myFileDialog) { return; } const QUrl newUrl = myFileDialog->selectedUrls().constFirst(); if (newUrl.isValid()) { m_parent->setUrl(newUrl); Q_EMIT m_parent->urlSelected(url()); // remember url as defaultStartDir and update startdir for autocompletion if (newUrl.isLocalFile() && !m_startDirCustomized) { m_startDir = newUrl.adjusted(QUrl::RemoveFilename); updateCompletionStartDir(m_startDir); } } } void KUrlRequester::setMode(KFile::Modes mode) { Q_ASSERT((mode & KFile::Files) == 0); d->fileDialogMode = mode; if ((mode & KFile::Directory) && !(mode & KFile::File)) { d->myCompletion->setMode(KUrlCompletion::DirCompletion); } if (d->myFileDialog) { d->applyFileMode(d->myFileDialog, mode, d->fileDialogAcceptMode); } } KFile::Modes KUrlRequester::mode() const { return d->fileDialogMode; } void KUrlRequester::setAcceptMode(QFileDialog::AcceptMode mode) { d->fileDialogAcceptMode = mode; if (d->myFileDialog) { d->applyFileMode(d->myFileDialog, d->fileDialogMode, mode); } } QFileDialog::AcceptMode KUrlRequester::acceptMode() const { return d->fileDialogAcceptMode; } QStringList KUrlRequester::nameFilters() const { return d->nameFilters; } void KUrlRequester::setNameFilters(const QStringList &filters) { d->nameFilters = filters; if (d->myFileDialog) { d->myFileDialog->setNameFilters(d->nameFilters); } } void KUrlRequester::setNameFilter(const QString &filter) { if (filter.isEmpty()) { setNameFilters(QStringList()); return; } // by default use ";;" as separator // if not present, support alternatively "\n" (matching QFileDialog behaviour) // if also not present split() will simply return the string passed in QString separator = QStringLiteral(";;"); if (!filter.contains(separator)) { separator = QStringLiteral("\n"); } setNameFilters(filter.split(separator)); } void KUrlRequester::setMimeTypeFilters(const QStringList &mimeTypes) { d->mimeTypeFilters = mimeTypes; if (d->myFileDialog) { d->myFileDialog->setMimeTypeFilters(d->mimeTypeFilters); } d->myCompletion->setMimeTypeFilters(d->mimeTypeFilters); } QStringList KUrlRequester::mimeTypeFilters() const { return d->mimeTypeFilters; } QFileDialog *KUrlRequester::fileDialog() const { if (d->myFileDialog && ((d->myFileDialog->fileMode() == QFileDialog::Directory && !(d->fileDialogMode & KFile::Directory)) || (d->myFileDialog->fileMode() != QFileDialog::Directory && (d->fileDialogMode & KFile::Directory)))) { delete d->myFileDialog; d->myFileDialog = nullptr; } if (!d->myFileDialog) { d->myFileDialog = new QFileDialog(window(), windowTitle()); if (!d->mimeTypeFilters.isEmpty()) { d->myFileDialog->setMimeTypeFilters(d->mimeTypeFilters); } else { d->myFileDialog->setNameFilters(d->nameFilters); } d->applyFileMode(d->myFileDialog, d->fileDialogMode, d->fileDialogAcceptMode); d->myFileDialog->setWindowModality(d->fileDialogModality); connect(d->myFileDialog, &QFileDialog::accepted, this, [this]() { d->slotFileDialogAccepted(); }); } return d->myFileDialog; } void KUrlRequester::clear() { d->setText(QString()); } KLineEdit *KUrlRequester::lineEdit() const { return d->edit; } KComboBox *KUrlRequester::comboBox() const { return d->combo; } void KUrlRequester::KUrlRequesterPrivate::slotUpdateUrl() { const QUrl visibleUrl = url(); QUrl u = visibleUrl; if (visibleUrl.isRelative()) { u = QUrl::fromLocalFile(QDir::currentPath() + QLatin1Char('/')).resolved(visibleUrl); } myButton->setURL(u); } bool KUrlRequester::eventFilter(QObject *obj, QEvent *ev) { if ((d->edit == obj) || (d->combo == obj)) { if ((ev->type() == QEvent::FocusIn) || (ev->type() == QEvent::FocusOut)) // Forward focusin/focusout events to the urlrequester; needed by file form element in khtml { QApplication::sendEvent(this, ev); } } return QWidget::eventFilter(obj, ev); } QPushButton *KUrlRequester::button() const { return d->myButton; } KUrlCompletion *KUrlRequester::completionObject() const { return d->myCompletion; } void KUrlRequester::setPlaceholderText(const QString &msg) { if (d->edit) { d->edit->setPlaceholderText(msg); } } QString KUrlRequester::placeholderText() const { if (d->edit) { return d->edit->placeholderText(); } else { return QString(); } } Qt::WindowModality KUrlRequester::fileDialogModality() const { return d->fileDialogModality; } void KUrlRequester::setFileDialogModality(Qt::WindowModality modality) { d->fileDialogModality = modality; } const KEditListWidget::CustomEditor &KUrlRequester::customEditor() { setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed)); KLineEdit *edit = d->edit; if (!edit && d->combo) { edit = qobject_cast(d->combo->lineEdit()); } #ifndef NDEBUG if (!edit) { qCWarning(EDITOR_LOG) << "KUrlRequester's lineedit is not a KLineEdit!??\n"; } #endif d->editor.setRepresentationWidget(this); d->editor.setLineEdit(edit); return d->editor; } KUrlComboRequester::KUrlComboRequester(QWidget *parent) : KUrlRequester(new KComboBox(false), parent) , d(nullptr) { } -#include "urlrequester.moc" #include "moc_urlrequester.cpp" +#include "urlrequester.moc" diff --git a/client/editor/util.cpp b/client/editor/util.cpp index f3164ca..ec9b2f1 100644 --- a/client/editor/util.cpp +++ b/client/editor/util.cpp @@ -1,439 +1,445 @@ /* SPDX-FileCopyrightText: 2009 Constantin Berzan SPDX-FileCopyrightText: 2009 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.net SPDX-FileCopyrightText: 2009 Leo Franchi Parts based on KMail code by: SPDX-License-Identifier: LGPL-2.0-or-later */ #include "util.h" #include "util_p.h" #include #include #include #include #include "editor_debug.h" #include #include #include #include #include #include -#include "job/singlepartjob.h" #include "composer.h" +#include "job/singlepartjob.h" static QString stripOffPrefixes(const QString &subject) { const QStringList replyPrefixes = { QStringLiteral("Re\\s*:"), QStringLiteral("Re\\[\\d+\\]:"), QStringLiteral("Re\\d+:"), }; const QStringList forwardPrefixes = { QStringLiteral("Fwd:"), QStringLiteral("FW:<"), }; const QStringList prefixRegExps = replyPrefixes + forwardPrefixes; // construct a big regexp that // 1. is anchored to the beginning of str (sans whitespace) // 2. matches at least one of the part regexps in prefixRegExps const QString bigRegExp = QStringLiteral("^(?:\\s+|(?:%1))+\\s*").arg(prefixRegExps.join(QStringLiteral(")|(?:"))); static QRegularExpression regex; if (regex.pattern() != bigRegExp) { // the prefixes have changed, so update the regexp regex.setPattern(bigRegExp); regex.setPatternOptions(QRegularExpression::CaseInsensitiveOption); } if (regex.isValid()) { QRegularExpressionMatch match = regex.match(subject); if (match.hasMatch()) { return subject.mid(match.capturedEnd(0)); } } else { qCWarning(EDITOR_LOG) << "bigRegExp = \"" << bigRegExp << "\"\n" - << "prefix regexp is invalid!"; + << "prefix regexp is invalid!"; } return subject; } KMime::Content *setBodyAndCTE(QByteArray &encodedBody, KMime::Headers::ContentType *contentType, KMime::Content *ret) { MessageComposer::Composer composer; MessageComposer::SinglepartJob cteJob(&composer); cteJob.contentType()->setMimeType(contentType->mimeType()); cteJob.contentType()->setCharset(contentType->charset()); cteJob.setData(encodedBody); cteJob.exec(); cteJob.content()->assemble(); ret->contentTransferEncoding()->setEncoding(cteJob.contentTransferEncoding()->encoding()); ret->setBody(cteJob.content()->encodedBody()); return ret; } KMime::Content *MessageComposer::Util::composeHeadersAndBody(KMime::Content *orig, QByteArray encodedBody, Kleo::CryptoMessageFormat format, bool sign, const QByteArray &hashAlgo) { auto result = new KMime::Content; // called should have tested that the signing/encryption failed Q_ASSERT(!encodedBody.isEmpty()); if (!(format & Kleo::InlineOpenPGPFormat)) { // make a MIME message qCDebug(EDITOR_LOG) << "making MIME message, format:" << format; makeToplevelContentType(result, format, sign, hashAlgo); if (makeMultiMime(format, sign)) { // sign/enc PGPMime, sign SMIME const QByteArray boundary = KMime::multiPartBoundary(); result->contentType()->setBoundary(boundary); result->assemble(); // qCDebug(EDITOR_LOG) << "processed header:" << result->head(); // Build the encapsulated MIME parts. // Build a MIME part holding the code information // taking the body contents returned in ciphertext. auto code = new KMime::Content; setNestedContentType(code, format, sign); setNestedContentDisposition(code, format, sign); if (sign) { // sign PGPMime, sign SMIME if (format & Kleo::AnySMIME) { // sign SMIME auto ct = code->contentTransferEncoding(); // create ct->setEncoding(KMime::Headers::CEbase64); ct->needToEncode(); code->setBody(encodedBody); } else { // sign PGPMmime setBodyAndCTE(encodedBody, orig->contentType(), code); } result->appendContent(orig); result->appendContent(code); } else { // enc PGPMime setBodyAndCTE(encodedBody, orig->contentType(), code); auto header = new KMime::Headers::Generic("PGP"); header->fromUnicodeString(QStringLiteral("encrypted"), "utf-8"); result->setHeader(header); auto instruction = new KMime::Content; instruction->contentType()->setMimeType("text/plain"); instruction->contentTransferEncoding()->setEncoding(KMime::Headers::CE7Bit); if (Kleo::DeVSCompliance::isCompliant()) { - instruction->setBody("This message is VS-NfD compliant encrypted and need to be opened in a compliant E-Mail client. For example: GnuPG VS-Desktop®"); + instruction->setBody( + "This message is VS-NfD compliant encrypted and need to be opened in a compliant E-Mail client. For example: GnuPG VS-Desktop®"); } else { - instruction->setBody("This message is encrypted with OpenPGP and need to be opened in a compatible client. For example: GPG4Win, KMail, Kleopatra or Thunderbird"); + instruction->setBody( + "This message is encrypted with OpenPGP and need to be opened in a compatible client. For example: GPG4Win, KMail, Kleopatra or " + "Thunderbird"); } auto encryptedPart = new KMime::Content; auto ct = encryptedPart->contentType(); // Create ct->setMimeType(QByteArrayLiteral("multipart/encrypted")); ct->setParameter(QStringLiteral("protocol"), QStringLiteral("application/pgp-encrypted")); const QByteArray boundary = KMime::multiPartBoundary(); ct->setBoundary(boundary); // Build a MIME part holding the version information // taking the body contents returned in // structuring.data.bodyTextVersion. auto vers = new KMime::Content; vers->contentType()->setMimeType("application/pgp-encrypted"); vers->contentDisposition()->setDisposition(KMime::Headers::CDattachment); vers->contentTransferEncoding()->setEncoding(KMime::Headers::CE7Bit); vers->setBody("Version: 1"); encryptedPart->appendContent(vers); encryptedPart->appendContent(code); result->appendContent(instruction); result->appendContent(encryptedPart); } } else { // enc SMIME, sign/enc SMIMEOpaque const QByteArray boundary = KMime::multiPartBoundary(); result->contentType()->setBoundary(boundary); result->assemble(); // qCDebug(EDITOR_LOG) << "processed header:" << result->head(); // Build the encapsulated MIME parts. // Build a MIME part holding the code information // taking the body contents returned in ciphertext. auto code = new KMime::Content; setNestedContentType(code, format, sign); setNestedContentDisposition(code, format, sign); setBodyAndCTE(encodedBody, orig->contentType(), code); code->assemble(); code->setBody(encodedBody); auto instruction = new KMime::Content; instruction->contentType()->setMimeType("text/plain"); instruction->contentTransferEncoding()->setEncoding(KMime::Headers::CE7Bit); if (Kleo::DeVSCompliance::isCompliant()) { - instruction->setBody("This message is VS-NfD compliant encrypted and need to be opened in a compliant E-Mail client. For example: GnuPG VS-Desktop®"); + instruction->setBody( + "This message is VS-NfD compliant encrypted and need to be opened in a compliant E-Mail client. For example: GnuPG VS-Desktop®"); } else { - instruction->setBody("This message is encrypted with S/MIME and need to be opened in a compatible client. For example: GPG4Win, KMail, Kleopatra or Thunderbird"); + instruction->setBody( + "This message is encrypted with S/MIME and need to be opened in a compatible client. For example: GPG4Win, KMail, Kleopatra or " + "Thunderbird"); } result->appendContent(instruction); result->appendContent(code); } } else { // sign/enc PGPInline result->setHead(orig->head()); result->parse(); // fixing ContentTransferEncoding setBodyAndCTE(encodedBody, orig->contentType(), result); } return result; } // set the correct top-level ContentType on the message void MessageComposer::Util::makeToplevelContentType(KMime::Content *content, Kleo::CryptoMessageFormat format, bool sign, const QByteArray &hashAlgo) { switch (format) { default: case Kleo::InlineOpenPGPFormat: case Kleo::OpenPGPMIMEFormat: { auto ct = content->contentType(); // Create if (sign) { ct->setMimeType(QByteArrayLiteral("multipart/signed")); ct->setParameter(QStringLiteral("protocol"), QStringLiteral("application/pgp-signature")); ct->setParameter(QStringLiteral("micalg"), QString::fromLatin1(QByteArray(QByteArrayLiteral("pgp-") + hashAlgo)).toLower()); } else { ct->setMimeType(QByteArrayLiteral("multipart/encrypted")); ct->setParameter(QStringLiteral("protocol"), QStringLiteral("application/pgp-encrypted")); } } return; case Kleo::SMIMEOpaqueFormat: case Kleo::SMIMEFormat: { auto ct = content->contentType(); // Create if (sign) { qCDebug(EDITOR_LOG) << "setting headers for SMIME"; ct->setMimeType(QByteArrayLiteral("multipart/signed")); ct->setParameter(QStringLiteral("protocol"), QStringLiteral("application/pkcs7-signature")); ct->setParameter(QStringLiteral("micalg"), QString::fromLatin1(hashAlgo).toLower()); return; } else { ct->setMimeType(QByteArrayLiteral("multipart/encrypted")); ct->setParameter(QStringLiteral("protocol"), QStringLiteral("application/smime-encrypted")); } // fall through (for encryption, there's no difference between // SMIME and SMIMEOpaque, since there is no mp/encrypted for // S/MIME) } } } void MessageComposer::Util::setNestedContentType(KMime::Content *content, Kleo::CryptoMessageFormat format, bool sign) { switch (format) { case Kleo::OpenPGPMIMEFormat: { auto ct = content->contentType(); // Create if (sign) { ct->setMimeType(QByteArrayLiteral("application/pgp-signature")); ct->setParameter(QStringLiteral("name"), QStringLiteral("signature.asc")); content->contentDescription()->from7BitString("This is a digitally signed message part."); } else { ct->setMimeType(QByteArrayLiteral("application/octet-stream")); } } return; case Kleo::SMIMEFormat: { auto ct = content->contentType(); // Create if (sign) { ct->setMimeType(QByteArrayLiteral("application/pkcs7-signature")); ct->setParameter(QStringLiteral("name"), QStringLiteral("smime.p7s")); ct->setParameter(QStringLiteral("smime-type"), QStringLiteral("signed-data")); } else { auto ct = content->contentType(); // Create ct->setMimeType(QByteArrayLiteral("application/pkcs7-mime")); ct->setParameter(QStringLiteral("name"), QStringLiteral("smime.p7m")); ct->setParameter(QStringLiteral("smime-type"), QStringLiteral("enveloped-data")); } return; } default: case Kleo::InlineOpenPGPFormat: case Kleo::SMIMEOpaqueFormat:; } } void MessageComposer::Util::setNestedContentDisposition(KMime::Content *content, Kleo::CryptoMessageFormat format, bool sign) { auto ct = content->contentDisposition(); if (!sign && format & Kleo::OpenPGPMIMEFormat) { ct->setDisposition(KMime::Headers::CDinline); ct->setFilename(QStringLiteral("msg.asc")); } else if (sign && format & Kleo::SMIMEFormat) { ct->setDisposition(KMime::Headers::CDattachment); ct->setFilename(QStringLiteral("smime.p7s")); } } bool MessageComposer::Util::makeMultiMime(Kleo::CryptoMessageFormat format, bool sign) { switch (format) { default: case Kleo::InlineOpenPGPFormat: case Kleo::SMIMEOpaqueFormat: return false; case Kleo::OpenPGPMIMEFormat: return true; case Kleo::SMIMEFormat: return sign; // only on sign - there's no mp/encrypted for S/MIME } } QByteArray MessageComposer::Util::selectCharset(const QList &charsets, const QString &text) { for (const QByteArray &name : charsets) { // We use KCharsets::codecForName() instead of QTextCodec::codecForName() here, because // the former knows us-ascii is latin1. QStringEncoder codec(name.constData()); if (!codec.isValid()) { qCWarning(EDITOR_LOG) << "Could not get text codec for charset" << name; continue; } if (codec.encode(text); !codec.hasError()) { // Special check for us-ascii (needed because us-ascii is not exactly latin1). if (name == "us-ascii" && !KMime::isUsAscii(text)) { continue; } qCDebug(EDITOR_LOG) << "Chosen charset" << name; return name; } } qCDebug(EDITOR_LOG) << "No appropriate charset found."; return {}; } QStringList MessageComposer::Util::AttachmentKeywords() { return i18nc( "comma-separated list of keywords that are used to detect whether " "the user forgot to attach his attachment. Do not add space between words.", "attachment,attached") .split(QLatin1Char(',')); } QString MessageComposer::Util::cleanedUpHeaderString(const QString &s) { // remove invalid characters from the header strings QString res(s); res.remove(QChar::fromLatin1('\r')); res.replace(QChar::fromLatin1('\n'), QLatin1Char(' ')); return res.trimmed(); } KMime::Content *MessageComposer::Util::findTypeInMessage(KMime::Content *data, const QByteArray &mimeType, const QByteArray &subType) { if (!data->contentType()->isEmpty()) { if (mimeType.isEmpty() || subType.isEmpty()) { return data; } if ((mimeType == data->contentType()->mediaType()) && (subType == data->contentType(false)->subType())) { return data; } } const auto contents = data->contents(); for (auto child : contents) { if ((!child->contentType()->isEmpty()) && (mimeType == child->contentType()->mimeType()) && (subType == child->contentType()->subType())) { return child; } auto ret = findTypeInMessage(child, mimeType, subType); if (ret) { return ret; } } return nullptr; } bool MessageComposer::Util::hasMissingAttachments(const QStringList &attachmentKeywords, QTextDocument *doc, const QString &subj) { if (!doc) { return false; } QStringList attachWordsList = attachmentKeywords; QRegularExpression rx(QLatin1String("\\b") + attachWordsList.join(QLatin1String("\\b|\\b")) + QLatin1String("\\b"), QRegularExpression::CaseInsensitiveOption); // check whether the subject contains one of the attachment key words // unless the message is a reply or a forwarded message bool gotMatch = (stripOffPrefixes(subj) == subj) && (rx.match(subj).hasMatch()); if (!gotMatch) { // check whether the non-quoted text contains one of the attachment key // words static QRegularExpression quotationRx(QStringLiteral("^([ \\t]*([|>:}#]|[A-Za-z]+>))+")); QTextBlock end(doc->end()); for (QTextBlock it = doc->begin(); it != end; it = it.next()) { const QString line = it.text(); gotMatch = (!quotationRx.match(line).hasMatch()) && (rx.match(line).hasMatch()); if (gotMatch) { break; } } } if (!gotMatch) { return false; } return true; } static QStringList encodeIdn(const QStringList &emails) { QStringList encoded; encoded.reserve(emails.count()); for (const QString &email : emails) { encoded << KEmailAddress::normalizeAddressesAndEncodeIdn(email); } return encoded; } QStringList MessageComposer::Util::cleanEmailList(const QStringList &emails) { QStringList clean; clean.reserve(emails.count()); for (const QString &email : emails) { clean << KEmailAddress::extractEmailAddress(email); } return clean; } QStringList MessageComposer::Util::cleanUpEmailListAndEncoding(const QStringList &emails) { return cleanEmailList(encodeIdn(emails)); } void MessageComposer::Util::addCustomHeaders(const KMime::Message::Ptr &message, const QMap &custom) { QMapIterator customHeader(custom); while (customHeader.hasNext()) { customHeader.next(); auto header = new KMime::Headers::Generic(customHeader.key().constData()); header->fromUnicodeString(customHeader.value(), "utf-8"); message->setHeader(header); } } diff --git a/client/ews/ewsmailfactory.cpp b/client/ews/ewsmailfactory.cpp index 7bf7e1a..9577f38 100644 --- a/client/ews/ewsmailfactory.cpp +++ b/client/ews/ewsmailfactory.cpp @@ -1,75 +1,75 @@ // SPDX-FileCopyrightText: 2015-2017 Krzysztof Nowicki // SPDX-FileCopyrightText: 2023 Carl Schwan // // SPDX-License-Identifier: LGPL-2.0-or-later #include "ewsmailfactory.h" #include +#include "ewsitem.h" #include "ewspropertyfield.h" #include "ewsserverversion.h" -#include "ewsitem.h" static const EwsPropertyField propPidMessageFlags(0x0e07, EwsPropTypeInteger); void startSoapDocument(QXmlStreamWriter &writer) { writer.writeStartDocument(); writer.writeNamespace(soapEnvNsUri, QStringLiteral("soap")); writer.writeNamespace(ewsMsgNsUri, QStringLiteral("m")); writer.writeNamespace(ewsTypeNsUri, QStringLiteral("t")); // SOAP Envelope writer.writeStartElement(soapEnvNsUri, QStringLiteral("Envelope")); // SOAP Header writer.writeStartElement(soapEnvNsUri, QStringLiteral("Header")); EwsServerVersion::ewsVersion2007Sp1.writeRequestServerVersion(writer); writer.writeEndElement(); // SOAP Body writer.writeStartElement(soapEnvNsUri, QStringLiteral("Body")); } void endSoapDocument(QXmlStreamWriter &writer) { // End SOAP Body writer.writeEndElement(); // End SOAP Envelope writer.writeEndElement(); writer.writeEndDocument(); } QString EwsMailFactory::create(const KMime::Message::Ptr &message) { const QByteArray mimeContent = message->encodedContent(true); EwsItem item; item.setType(EwsItemTypeMessage); item.setField(EwsItemFieldMimeContent, mimeContent); QString reqString; QXmlStreamWriter writer(&reqString); startSoapDocument(writer); writer.writeStartElement(ewsMsgNsUri, QStringLiteral("CreateItem")); writer.writeAttribute(QStringLiteral("MessageDisposition"), QStringLiteral("SendOnly")); writer.writeStartElement(ewsMsgNsUri, QStringLiteral("Items")); item.write(writer); writer.writeEndElement(); writer.writeEndElement(); endSoapDocument(writer); return reqString; } #include "moc_ewsmailfactory.cpp" diff --git a/client/ews/ewsmailfactory.h b/client/ews/ewsmailfactory.h index d76a5ba..e8612ca 100644 --- a/client/ews/ewsmailfactory.h +++ b/client/ews/ewsmailfactory.h @@ -1,14 +1,14 @@ // SPDX-FileCopyrightText: 2023 Carl Schwan // SPDX-License-Identifier: LGPL-2.0-or-later #pragma once -#include #include +#include class EwsItem; namespace EwsMailFactory { - QString create(const KMime::Message::Ptr &message); +QString create(const KMime::Message::Ptr &message); }; diff --git a/client/ews/ewsxml.cpp b/client/ews/ewsxml.cpp index 7b77e79..9f03e82 100644 --- a/client/ews/ewsxml.cpp +++ b/client/ews/ewsxml.cpp @@ -1,335 +1,336 @@ /* SPDX-FileCopyrightText: 2015-2017 Krzysztof Nowicki SPDX-License-Identifier: LGPL-2.0-or-later */ #include "ewsxml.h" #include #include "ewsid.h" #include "ewsitem.h" static constexpr auto messageSensitivityNames = std::to_array({ QLatin1StringView("Normal"), QLatin1StringView("Personal"), QLatin1StringView("Private"), QLatin1StringView("Confidential"), }); static constexpr auto messageImportanceNames = std::to_array({ QLatin1StringView("Low"), QLatin1StringView("Normal"), QLatin1StringView("High"), }); static constexpr auto calendarItemTypeNames = std::to_array({ QLatin1StringView("Single"), QLatin1StringView("Occurrence"), QLatin1StringView("Exception"), QLatin1StringView("RecurringMaster"), }); static constexpr auto legacyFreeBusyStatusNames = std::to_array({ QLatin1StringView("Free"), QLatin1StringView("Tentative"), QLatin1StringView("Busy"), QLatin1StringView("OOF"), QLatin1StringView("NoData"), }); static constexpr auto responseTypeNames = std::to_array({ QLatin1StringView("Unknown"), QLatin1StringView("Organizer"), QLatin1StringView("Tentative"), QLatin1StringView("Accept"), QLatin1StringView("Decline"), QLatin1StringView("NoResponseReceived"), }); bool ewsXmlBoolReader(QXmlStreamReader &reader, QVariant &val) { const QString elmText = reader.readElementText(); if (reader.error() != QXmlStreamReader::NoError) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Error reading %1 element").arg(reader.name().toString()); reader.skipCurrentElement(); return false; } if (elmText == QLatin1String("true")) { val = true; } else if (elmText == QLatin1String("false")) { val = false; } else { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Unexpected invalid boolean value in %1 element:").arg(reader.name().toString()) << elmText; return false; } return true; } bool ewsXmlBoolWriter(QXmlStreamWriter &writer, const QVariant &val) { writer.writeCharacters(val.toBool() ? QStringLiteral("true") : QStringLiteral("false")); return true; } bool ewsXmlBase64Reader(QXmlStreamReader &reader, QVariant &val) { QString elmName = reader.name().toString(); val = QByteArray::fromBase64(reader.readElementText().toLatin1()); if (reader.error() != QXmlStreamReader::NoError) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid content.").arg(elmName); reader.skipCurrentElement(); return false; } return true; } bool ewsXmlBase64Writer(QXmlStreamWriter &writer, const QVariant &val) { writer.writeCharacters(QString::fromLatin1(val.toByteArray().toBase64())); return true; } bool ewsXmlIdReader(QXmlStreamReader &reader, QVariant &val) { QString elmName = reader.name().toString(); EwsId id = EwsId(reader); if (id.type() == EwsId::Unspecified) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid content.").arg(elmName); reader.skipCurrentElement(); return false; } val = QVariant::fromValue(id); reader.skipCurrentElement(); return true; } bool ewsXmlIdWriter(QXmlStreamWriter &writer, const QVariant &val) { EwsId id = val.value(); if (id.type() == EwsId::Unspecified) { return false; } id.writeAttributes(writer); return true; } bool ewsXmlTextReader(QXmlStreamReader &reader, QVariant &val) { QString elmName = reader.name().toString(); val = reader.readElementText(); if (reader.error() != QXmlStreamReader::NoError) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid content.").arg(elmName); reader.skipCurrentElement(); return false; } return true; } bool ewsXmlTextWriter(QXmlStreamWriter &writer, const QVariant &val) { writer.writeCharacters(val.toString()); return true; } bool ewsXmlUIntReader(QXmlStreamReader &reader, QVariant &val) { QString elmName = reader.name().toString(); bool ok; val = reader.readElementText().toUInt(&ok); if (reader.error() != QXmlStreamReader::NoError || !ok) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid content.").arg(elmName); return false; } return true; } bool ewsXmlUIntWriter(QXmlStreamWriter &writer, const QVariant &val) { writer.writeCharacters(QString::number(val.toUInt())); return true; } bool ewsXmlDateTimeReader(QXmlStreamReader &reader, QVariant &val) { QString elmName = reader.name().toString(); QDateTime dt = QDateTime::fromString(reader.readElementText(), Qt::ISODate); if (reader.error() != QXmlStreamReader::NoError || !dt.isValid()) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid content.").arg(elmName); return false; } val = QVariant::fromValue(dt); return true; } bool ewsXmlItemReader(QXmlStreamReader &reader, QVariant &val) { QString elmName = reader.name().toString(); EwsItem item = EwsItem(reader); if (!item.isValid()) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid content.").arg(elmName); reader.skipCurrentElement(); return false; } val = QVariant::fromValue(item); return true; } template bool ewsXmlEnumReader(QXmlStreamReader &reader, QVariant &val, const std::array &items) { const QString elmName = reader.name().toString(); QString text = reader.readElementText(); if (reader.error() != QXmlStreamReader::NoError) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid content.").arg(elmName); reader.skipCurrentElement(); return false; } int i = 0; - auto it = items.cbegin();; + auto it = items.cbegin(); + ; for (; it != items.cend(); ++it, i++) { if (text == *it) { val = i; break; } } if (it == items.cend()) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - unknown value %2.").arg(elmName, text); return false; } return true; } bool ewsXmlSensitivityReader(QXmlStreamReader &reader, QVariant &val) { return ewsXmlEnumReader(reader, val, messageSensitivityNames); } bool ewsXmlImportanceReader(QXmlStreamReader &reader, QVariant &val) { return ewsXmlEnumReader(reader, val, messageImportanceNames); } bool ewsXmlCalendarItemTypeReader(QXmlStreamReader &reader, QVariant &val) { return ewsXmlEnumReader(reader, val, calendarItemTypeNames); } bool ewsXmlLegacyFreeBusyStatusReader(QXmlStreamReader &reader, QVariant &val) { return ewsXmlEnumReader(reader, val, legacyFreeBusyStatusNames); } bool ewsXmlResponseTypeReader(QXmlStreamReader &reader, QVariant &val) { return ewsXmlEnumReader(reader, val, responseTypeNames); } template<> QString readXmlElementValue(QXmlStreamReader &reader, bool &ok, const QString &parentElement) { ok = true; const QStringView elmName = reader.name(); QString val = reader.readElementText(); if (reader.error() != QXmlStreamReader::NoError) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid %2 element.").arg(parentElement, elmName.toString()); reader.skipCurrentElement(); val.clear(); ok = false; } return val; } template<> int readXmlElementValue(QXmlStreamReader &reader, bool &ok, const QString &parentElement) { const QStringView elmName = reader.name(); QString valStr = readXmlElementValue(reader, ok, parentElement); int val = 0; if (ok) { val = valStr.toInt(&ok); if (!ok) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid %2 element.").arg(parentElement, elmName.toString()); } } return val; } template<> long readXmlElementValue(QXmlStreamReader &reader, bool &ok, const QString &parentElement) { const QStringView elmName = reader.name(); QString valStr = readXmlElementValue(reader, ok, parentElement); long val = 0; if (ok) { val = valStr.toLong(&ok); if (!ok) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid %2 element.").arg(parentElement, elmName.toString()); } } return val; } template<> QDateTime readXmlElementValue(QXmlStreamReader &reader, bool &ok, const QString &parentElement) { const QStringView elmName = reader.name(); QString valStr = readXmlElementValue(reader, ok, parentElement); QDateTime val; if (ok) { val = QDateTime::fromString(valStr, Qt::ISODate); if (!val.isValid()) { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid %2 element.").arg(parentElement, elmName.toString()); ok = false; } } return val; } template<> bool readXmlElementValue(QXmlStreamReader &reader, bool &ok, const QString &parentElement) { const QStringView elmName = reader.name(); QString valStr = readXmlElementValue(reader, ok, parentElement); bool val = false; if (ok) { if (valStr == QLatin1String("true")) { val = true; } else if (valStr == QLatin1String("false")) { val = false; } else { qCWarningNC(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid %2 element.").arg(parentElement, elmName.toString()); ok = false; } } return val; } template<> QByteArray readXmlElementValue(QXmlStreamReader &reader, bool &ok, const QString &parentElement) { QString valStr = readXmlElementValue(reader, ok, parentElement); QByteArray val; if (ok) { /* QByteArray::fromBase64() does not perform any input validity checks and skips invalid input characters */ val = QByteArray::fromBase64(valStr.toLatin1()); } return val; } diff --git a/client/ews/ewsxml.h b/client/ews/ewsxml.h index dbff5ed..c61a37f 100644 --- a/client/ews/ewsxml.h +++ b/client/ews/ewsxml.h @@ -1,179 +1,178 @@ /* SPDX-FileCopyrightText: 2015-2017 Krzysztof Nowicki SPDX-License-Identifier: LGPL-2.0-or-later */ #pragma once #include #include #include #include "ewsclient_debug.h" template class EwsXml { public: typedef std::function ReadFunction; typedef std::function WriteFunction; typedef std::function UnknownElementFunction; typedef QHash ValueHash; static constexpr T Ignore = static_cast(-1); struct Item { Item() : key(Ignore) { } Item(T k, const QString &n, const ReadFunction &rfn = ReadFunction(), const WriteFunction &wfn = WriteFunction()) : key(k) , elmName(n) , readFn(rfn) , writeFn(wfn) { } T key; QString elmName; ReadFunction readFn; WriteFunction writeFn; }; EwsXml() { } EwsXml(const QList &items) : mItems(items) { rebuildItemHash(); } EwsXml(const EwsXml &other) : mItems(other.mItems) , mValues(other.mValues) , mItemHash(other.mItemHash) { } void setItems(const QList &items) { mItems = items; rebuildItemHash(); } bool readItem(QXmlStreamReader &reader, const QString &parentElm, const QString &nsUri, UnknownElementFunction unknownElmFn = &defaultUnknownElmFunction) { typename QHash::iterator it = mItemHash.find(reader.name().toString()); if (it != mItemHash.end() && nsUri == reader.namespaceUri()) { if (it->key == Ignore) { qCInfoNC(EWSCLI_LOG) << QStringLiteral("Unsupported %1 child element %2 - ignoring.").arg(parentElm, reader.name().toString()); reader.skipCurrentElement(); return true; } else if (!it->readFn) { - qCWarning(EWSCLI_LOG) - << QStringLiteral("Failed to read %1 element - no read support for %2 element.").arg(parentElm, reader.name().toString()); + qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - no read support for %2 element.").arg(parentElm, reader.name().toString()); return false; } else { QVariant val = mValues[it->key]; if (it->readFn(reader, val)) { mValues[it->key] = val; return true; } return false; } } return unknownElmFn(reader, parentElm); } bool readItems(QXmlStreamReader &reader, const QString &nsUri, const UnknownElementFunction &unknownElmFn = &defaultUnknownElmFunction) { QString elmName(reader.name().toString()); while (reader.readNextStartElement()) { if (!readItem(reader, elmName, nsUri, unknownElmFn)) { return false; } } return true; } bool writeItems(QXmlStreamWriter &writer, const QString &parentElm, const QString &nsUri, const ValueHash &values, const QList &keysToWrite = QList()) const { bool hasKeysToWrite = !keysToWrite.isEmpty(); for (const Item &item : std::as_const(mItems)) { if (!hasKeysToWrite || keysToWrite.contains(item.key)) { typename ValueHash::const_iterator it = values.find(item.key); if (it != values.end()) { if (!item.writeFn) { qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to write %1 element - no write support for %2 element.").arg(parentElm).arg(item.elmName); return false; } writer.writeStartElement(nsUri, item.elmName); bool status = item.writeFn(writer, *it); writer.writeEndElement(); if (!status) { return false; } } } } return true; } ValueHash values() const { return mValues; } private: static bool defaultUnknownElmFunction(QXmlStreamReader &reader, const QString &parentElm) { qCWarning(EWSCLI_LOG) << QStringLiteral("Failed to read %1 element - invalid %2 element.").arg(parentElm, reader.name().toString()); return false; } const QList mItems; ValueHash mValues; QHash mItemHash; void rebuildItemHash() { for (const Item &item : std::as_const(mItems)) { mItemHash.insert(item.elmName, item); } } }; template T readXmlElementValue(QXmlStreamReader &reader, bool &ok, const QString &parentElement); extern bool ewsXmlBoolReader(QXmlStreamReader &reader, QVariant &val); extern bool ewsXmlBoolWriter(QXmlStreamWriter &writer, const QVariant &val); extern bool ewsXmlBase64Reader(QXmlStreamReader &reader, QVariant &val); extern bool ewsXmlBase64Writer(QXmlStreamWriter &writer, const QVariant &val); extern bool ewsXmlIdReader(QXmlStreamReader &reader, QVariant &val); extern bool ewsXmlIdWriter(QXmlStreamWriter &writer, const QVariant &val); extern bool ewsXmlTextReader(QXmlStreamReader &reader, QVariant &val); extern bool ewsXmlTextWriter(QXmlStreamWriter &writer, const QVariant &val); extern bool ewsXmlUIntReader(QXmlStreamReader &reader, QVariant &val); extern bool ewsXmlUIntWriter(QXmlStreamWriter &writer, const QVariant &val); extern bool ewsXmlDateTimeReader(QXmlStreamReader &reader, QVariant &val); extern bool ewsXmlItemReader(QXmlStreamReader &reader, QVariant &val); extern bool ewsXmlEnumReader(QXmlStreamReader &reader, QVariant &val, const QList &items); extern bool ewsXmlSensitivityReader(QXmlStreamReader &reader, QVariant &val); extern bool ewsXmlImportanceReader(QXmlStreamReader &reader, QVariant &val); extern bool ewsXmlCalendarItemTypeReader(QXmlStreamReader &reader, QVariant &val); extern bool ewsXmlLegacyFreeBusyStatusReader(QXmlStreamReader &reader, QVariant &val); extern bool ewsXmlResponseTypeReader(QXmlStreamReader &reader, QVariant &val); diff --git a/client/identity/addressvalidationjob.cpp b/client/identity/addressvalidationjob.cpp index 319e5a0..6b5197b 100644 --- a/client/identity/addressvalidationjob.cpp +++ b/client/identity/addressvalidationjob.cpp @@ -1,53 +1,53 @@ /* * This file is part of KMail. * * SPDX-FileCopyrightText: 2010 KDAB * * SPDX-FileContributor: Tobias Koenig * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "addressvalidationjob.h" #include #include #include AddressValidationJob::AddressValidationJob(const QString &emailAddresses, QWidget *parentWidget, QObject *parent) : KJob(parent) , mEmailAddresses(emailAddresses) , mParentWidget(parentWidget) { } AddressValidationJob::~AddressValidationJob() = default; void AddressValidationJob::setDefaultDomain(const QString &domainName) { mDomainDefaultName = domainName; } void AddressValidationJob::start() { QString brokenAddress; const KEmailAddress::EmailParseResult errorCode = KEmailAddress::isValidAddressList(mEmailAddresses, brokenAddress); if (!(errorCode == KEmailAddress::AddressOk || errorCode == KEmailAddress::AddressEmpty)) { - const QString errorMsg(QLatin1String("

    ") + brokenAddress + QLatin1String("

    ") - + KEmailAddress::emailParseResultToString(errorCode) + QLatin1String("

    ")); + const QString errorMsg(QLatin1String("

    ") + brokenAddress + QLatin1String("

    ") + KEmailAddress::emailParseResultToString(errorCode) + + QLatin1String("

    ")); KMessageBox::error(mParentWidget, errorMsg, i18nc("@title:window", "Invalid Email Address")); mIsValid = false; } else { mIsValid = true; } emitResult(); } bool AddressValidationJob::isValid() const { return mIsValid; } #include "moc_addressvalidationjob.cpp" diff --git a/client/identity/identitydialog.cpp b/client/identity/identitydialog.cpp index 54ad125..4759d2f 100644 --- a/client/identity/identitydialog.cpp +++ b/client/identity/identitydialog.cpp @@ -1,606 +1,606 @@ /* identitydialog.cpp This file is part of KMail, the KDE mail client. SPDX-FileCopyrightText: 2002 Marc Mutz SPDX-FileCopyrightText: 2014-2023 Laurent Montel SPDX-License-Identifier: GPL-2.0-only */ #include "identitydialog.h" -#include "identitymanager.h" #include "addressvalidationjob.h" +#include "identitymanager.h" #include "kleo_util.h" #include #include // other KMail headers: #include #include // other kdepim headers: #include "identity/identity.h" #include "identity/signatureconfigurator.h" #include // libkleopatra: #include #include #include #include // gpgme++ #include // other KDE headers: #include #include +#include #include #include -#include // Qt headers: #include +#include +#include #include #include #include #include +#include #include -#include -#include -#include -#include #include +#include #include -#include -#include -#include #include +#include +#include +#include // other headers: #include #include #include - namespace KMail { class KeySelectionCombo : public Kleo::KeySelectionCombo { Q_OBJECT public: enum KeyType { SigningKey, EncryptionKey, }; explicit KeySelectionCombo(KeyType keyType, GpgME::Protocol protocol, QWidget *parent); ~KeySelectionCombo() override; void setIdentity(const QString &name, const QString &email); void init() override; private: void onCustomItemSelected(const QVariant &type); QString mEmail; QString mName; const KeyType mKeyType; const GpgME::Protocol mProtocol; }; class KeyGenerationJob : public QGpgME::Job { Q_OBJECT public: explicit KeyGenerationJob(const QString &name, const QString &email, KeySelectionCombo *parent); ~KeyGenerationJob() override; void slotCancel() override; void start(); private: void keyGenerated(const GpgME::KeyGenerationResult &result); const QString mName; const QString mEmail; QGpgME::Job *mJob = nullptr; }; KeyGenerationJob::KeyGenerationJob(const QString &name, const QString &email, KeySelectionCombo *parent) : QGpgME::Job(parent) , mName(name) , mEmail(email) { } KeyGenerationJob::~KeyGenerationJob() = default; void KeyGenerationJob::slotCancel() { if (mJob) { mJob->slotCancel(); } } void KeyGenerationJob::start() { auto job = new Kleo::DefaultKeyGenerationJob(this); connect(job, &Kleo::DefaultKeyGenerationJob::result, this, &KeyGenerationJob::keyGenerated); job->start(mEmail, mName); mJob = job; } void KeyGenerationJob::keyGenerated(const GpgME::KeyGenerationResult &result) { mJob = nullptr; if (result.error()) { KMessageBox::error(qobject_cast(parent()), i18n("Error while generating new key pair: %1", QString::fromUtf8(result.error().asString())), i18n("Key Generation Error")); Q_EMIT done(); return; } auto combo = qobject_cast(parent()); combo->setDefaultKey(QLatin1String(result.fingerprint())); connect(combo, &KeySelectionCombo::keyListingFinished, this, &KeyGenerationJob::done); combo->refreshKeys(); } KeySelectionCombo::KeySelectionCombo(KeyType keyType, GpgME::Protocol protocol, QWidget *parent) : Kleo::KeySelectionCombo(parent) , mKeyType(keyType) , mProtocol(protocol) { } KeySelectionCombo::~KeySelectionCombo() = default; void KeySelectionCombo::setIdentity(const QString &name, const QString &email) { mName = name; mEmail = email; setIdFilter(email); } void KeySelectionCombo::init() { Kleo::KeySelectionCombo::init(); std::shared_ptr keyFilter(new Kleo::DefaultKeyFilter); keyFilter->setIsOpenPGP(mProtocol == GpgME::OpenPGP ? Kleo::DefaultKeyFilter::Set : Kleo::DefaultKeyFilter::NotSet); if (mKeyType == SigningKey) { keyFilter->setCanSign(Kleo::DefaultKeyFilter::Set); keyFilter->setHasSecret(Kleo::DefaultKeyFilter::Set); } else { keyFilter->setCanEncrypt(Kleo::DefaultKeyFilter::Set); } setKeyFilter(keyFilter); prependCustomItem(QIcon(), i18n("No key"), QStringLiteral("no-key")); if (mProtocol == GpgME::OpenPGP) { appendCustomItem(QIcon::fromTheme(QStringLiteral("password-generate")), i18n("Generate a new key pair"), QStringLiteral("generate-new-key")); } connect(this, &KeySelectionCombo::customItemSelected, this, &KeySelectionCombo::onCustomItemSelected); } void KeySelectionCombo::onCustomItemSelected(const QVariant &type) { if (type == QLatin1String("no-key")) { return; } else if (type == QLatin1String("generate-new-key")) { auto job = new KeyGenerationJob(mName, mEmail, this); auto dlg = new Kleo::ProgressDialog(job, i18n("Generating new key pair..."), parentWidget()); dlg->setModal(true); setEnabled(false); connect(job, &KeyGenerationJob::done, this, [this]() { setEnabled(true); }); job->start(); } } IdentityDialog::IdentityDialog(QWidget *parent) : QDialog(parent) { setWindowTitle(i18nc("@title:window", "Edit Identity")); auto mainLayout = new QVBoxLayout(this); mainLayout->setContentsMargins({}); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, this, &IdentityDialog::slotAccepted); connect(buttonBox, &QDialogButtonBox::rejected, this, &IdentityDialog::reject); // // Tab Widget: General // auto page = new QWidget(this); mainLayout->addWidget(page); auto buttonBoxLayout = new QVBoxLayout; buttonBoxLayout->setContentsMargins(style()->pixelMetric(QStyle::PM_LayoutLeftMargin), style()->pixelMetric(QStyle::PM_LayoutTopMargin), style()->pixelMetric(QStyle::PM_LayoutRightMargin), style()->pixelMetric(QStyle::PM_LayoutBottomMargin)); buttonBoxLayout->addWidget(buttonBox); mainLayout->addLayout(buttonBoxLayout); auto vlay = new QVBoxLayout(page); vlay->setContentsMargins({}); mTabWidget = new QTabWidget(page); mTabWidget->tabBar()->setExpanding(true); mTabWidget->setDocumentMode(true); mTabWidget->setObjectName(QStringLiteral("config-identity-tab")); vlay->addWidget(mTabWidget); auto tab = new QWidget(mTabWidget); mTabWidget->addTab(tab, i18nc("@title:tab General identity settings.", "General")); auto formLayout = new QFormLayout(tab); // "Name" line edit and label: mNameEdit = new QLineEdit(tab); KLineEditEventHandler::catchReturnKey(mNameEdit); auto label = new QLabel(i18n("&Your name:"), tab); formLayout->addRow(label, mNameEdit); label->setBuddy(mNameEdit); QString msg = i18n( "

    Your name

    " "

    This field should contain your name as you would like " "it to appear in the email header that is sent out;

    " "

    if you leave this blank your real name will not " "appear, only the email address.

    "); label->setWhatsThis(msg); mNameEdit->setWhatsThis(msg); // "Organization" line edit and label: mOrganizationEdit = new QLineEdit(tab); KLineEditEventHandler::catchReturnKey(mOrganizationEdit); label = new QLabel(i18n("Organi&zation:"), tab); formLayout->addRow(label, mOrganizationEdit); label->setBuddy(mOrganizationEdit); msg = i18n( "

    Organization

    " "

    This field should have the name of your organization " "if you would like it to be shown in the email header that " "is sent out.

    " "

    It is safe (and normal) to leave this blank.

    "); label->setWhatsThis(msg); mOrganizationEdit->setWhatsThis(msg); // "Dictionary" combo box and label: mDictionaryCombo = new Sonnet::DictionaryComboBox(tab); label = new QLabel(i18n("D&ictionary:"), tab); label->setBuddy(mDictionaryCombo); formLayout->addRow(label, mDictionaryCombo); // "Email Address" line edit and label: // (row 3: spacer) mEmailEdit = new QLineEdit(tab); mEmailEdit->setEnabled(false); KLineEditEventHandler::catchReturnKey(mEmailEdit); label = new QLabel(i18n("&Email address:"), tab); formLayout->addRow(label, mEmailEdit); label->setBuddy(mEmailEdit); msg = i18n( "

    Email address

    " "

    This field should have your full email address.

    " "

    This address is the primary one, used for all outgoing mail. " "If you have more than one address, either create a new identity, " "or add additional alias addresses in the field below.

    " "

    If you leave this blank, or get it wrong, people " "will have trouble replying to you.

    "); label->setWhatsThis(msg); mEmailEdit->setWhatsThis(msg); auto emailValidator = new KEmailValidator(this); mEmailEdit->setValidator(emailValidator); // // Tab Widget: Security // mCryptographyTab = new QWidget(mTabWidget); mTabWidget->addTab(mCryptographyTab, i18nc("@title:tab", "Security")); formLayout = new QFormLayout(mCryptographyTab); // "OpenPGP Signature Key" requester and label: mPGPSigningKeyRequester = new KeySelectionCombo(KeySelectionCombo::SigningKey, GpgME::OpenPGP, mCryptographyTab); mPGPSigningKeyRequester->setObjectName("PGP Signing Key Requester"); msg = i18n( "

    The OpenPGP key you choose here will be used " "to digitally sign messages. You can also use GnuPG keys.

    " "

    You can leave this blank, but KMail will not be able " "to digitally sign emails using OpenPGP; " "normal mail functions will not be affected.

    " "

    You can find out more about keys at https://www.gnupg.org

    "); label = new QLabel(i18n("OpenPGP signing key:"), mCryptographyTab); label->setBuddy(mPGPSigningKeyRequester); mPGPSigningKeyRequester->setWhatsThis(msg); label->setWhatsThis(msg); auto vbox = new QVBoxLayout; mPGPSameKey = new QCheckBox(i18n("Use same key for encryption and signing")); vbox->addWidget(mPGPSigningKeyRequester); vbox->addWidget(mPGPSameKey); formLayout->addRow(label, vbox); connect(mPGPSameKey, &QCheckBox::toggled, this, [=](bool checked) { mPGPEncryptionKeyRequester->setVisible(!checked); formLayout->labelForField(mPGPEncryptionKeyRequester)->setVisible(!checked); const auto label = qobject_cast(formLayout->labelForField(vbox)); if (checked) { label->setText(i18n("OpenPGP key:")); const auto key = mPGPSigningKeyRequester->currentKey(); if (!key.isBad()) { mPGPEncryptionKeyRequester->setCurrentKey(key); } else if (mPGPSigningKeyRequester->currentData() == QLatin1String("no-key")) { mPGPEncryptionKeyRequester->setCurrentIndex(mPGPSigningKeyRequester->currentIndex()); } } else { label->setText(i18n("OpenPGP signing key:")); } }); connect(mPGPSigningKeyRequester, &KeySelectionCombo::currentKeyChanged, this, [&](const GpgME::Key &key) { if (mPGPSameKey->isChecked()) { mPGPEncryptionKeyRequester->setCurrentKey(key); } }); connect(mPGPSigningKeyRequester, &KeySelectionCombo::customItemSelected, this, [&](const QVariant &type) { if (mPGPSameKey->isChecked() && type == QLatin1String("no-key")) { mPGPEncryptionKeyRequester->setCurrentIndex(mPGPSigningKeyRequester->currentIndex()); } }); connect(mPGPSigningKeyRequester, &KeySelectionCombo::keyListingFinished, this, [this] { slotKeyListingFinished(mPGPSigningKeyRequester); }); // "OpenPGP Encryption Key" requester and label: mPGPEncryptionKeyRequester = new KeySelectionCombo(KeySelectionCombo::EncryptionKey, GpgME::OpenPGP, mCryptographyTab); msg = i18n( "

    The OpenPGP key you choose here will be used " "to encrypt messages to yourself and for the \"Attach My Public Key\" " "feature in the composer. You can also use GnuPG keys.

    " "

    You can leave this blank, but KMail will not be able " "to encrypt copies of outgoing messages to you using OpenPGP; " "normal mail functions will not be affected.

    " "

    You can find out more about keys at https://www.gnupg.org

    "); label = new QLabel(i18n("OpenPGP encryption key:"), mCryptographyTab); label->setBuddy(mPGPEncryptionKeyRequester); label->setWhatsThis(msg); mPGPEncryptionKeyRequester->setWhatsThis(msg); formLayout->addRow(label, mPGPEncryptionKeyRequester); // "S/MIME Signature Key" requester and label: mSMIMESigningKeyRequester = new KeySelectionCombo(KeySelectionCombo::SigningKey, GpgME::CMS, mCryptographyTab); mSMIMESigningKeyRequester->setObjectName("SMIME Signing Key Requester"); msg = i18n( "

    The S/MIME (X.509) certificate you choose here will be used " "to digitally sign messages.

    " "

    You can leave this blank, but KMail will not be able " "to digitally sign emails using S/MIME; " "normal mail functions will not be affected.

    "); label = new QLabel(i18n("S/MIME signing certificate:"), mCryptographyTab); label->setBuddy(mSMIMESigningKeyRequester); mSMIMESigningKeyRequester->setWhatsThis(msg); label->setWhatsThis(msg); formLayout->addRow(label, mSMIMESigningKeyRequester); connect(mSMIMESigningKeyRequester, &KeySelectionCombo::keyListingFinished, this, [this] { slotKeyListingFinished(mSMIMESigningKeyRequester); }); const QGpgME::Protocol *smimeProtocol = QGpgME::smime(); label->setEnabled(smimeProtocol); mSMIMESigningKeyRequester->setEnabled(smimeProtocol); // "S/MIME Encryption Key" requester and label: mSMIMEEncryptionKeyRequester = new KeySelectionCombo(KeySelectionCombo::EncryptionKey, GpgME::CMS, mCryptographyTab); mSMIMEEncryptionKeyRequester->setObjectName("SMIME Encryption Key Requester"); msg = i18n( "

    The S/MIME certificate you choose here will be used " "to encrypt messages to yourself and for the \"Attach My Certificate\" " "feature in the composer.

    " "

    You can leave this blank, but KMail will not be able " "to encrypt copies of outgoing messages to you using S/MIME; " "normal mail functions will not be affected.

    "); label = new QLabel(i18n("S/MIME encryption certificate:"), mCryptographyTab); label->setBuddy(mSMIMEEncryptionKeyRequester); mSMIMEEncryptionKeyRequester->setWhatsThis(msg); connect(mSMIMEEncryptionKeyRequester, &KeySelectionCombo::keyListingFinished, this, [this] { slotKeyListingFinished(mSMIMEEncryptionKeyRequester); }); label->setWhatsThis(msg); formLayout->addRow(label, mSMIMEEncryptionKeyRequester); label->setEnabled(smimeProtocol); mSMIMEEncryptionKeyRequester->setEnabled(smimeProtocol); mWarnNotEncrypt = new QCheckBox(i18n("Warn when trying to send unencrypted messages")); formLayout->addRow(QString(), mWarnNotEncrypt); // // Tab Widget: Signature // mSignatureConfigurator = new KIdentityManagementWidgets::SignatureConfigurator(mTabWidget); mTabWidget->addTab(mSignatureConfigurator, i18n("Signature")); } IdentityDialog::~IdentityDialog() = default; void IdentityDialog::slotAccepted() { // Validate email addresses const QString email = mEmailEdit->text().trimmed(); if (email.isEmpty()) { KMessageBox::error(this, i18n("You must provide an email for this identity."), i18nc("@title:window", "Empty Email Address")); return; } if (!KEmailAddress::isValidSimpleAddress(email)) { const QString errorMsg(KEmailAddress::simpleEmailAddressErrorMsg()); KMessageBox::error(this, errorMsg, i18n("Invalid Email Address")); return; } const GpgME::Key &pgpSigningKey = mPGPSigningKeyRequester->currentKey(); const GpgME::Key &pgpEncryptionKey = mPGPEncryptionKeyRequester->currentKey(); const GpgME::Key &smimeSigningKey = mSMIMESigningKeyRequester->currentKey(); const GpgME::Key &smimeEncryptionKey = mSMIMEEncryptionKeyRequester->currentKey(); QString msg; bool err = false; if (!keyMatchesEmailAddress(pgpSigningKey, email)) { msg = i18n( "One of the configured OpenPGP signing keys does not contain " "any user ID with the configured email address for this " "identity (%1).\n" "This might result in warning messages on the receiving side " "when trying to verify signatures made with this configuration.", email); err = true; } else if (!keyMatchesEmailAddress(pgpEncryptionKey, email)) { msg = i18n( "One of the configured OpenPGP encryption keys does not contain " "any user ID with the configured email address for this " "identity (%1).", email); err = true; } else if (!keyMatchesEmailAddress(smimeSigningKey, email)) { msg = i18n( "One of the configured S/MIME signing certificates does not contain " "the configured email address for this " "identity (%1).\n" "This might result in warning messages on the receiving side " "when trying to verify signatures made with this configuration.", email); err = true; } else if (!keyMatchesEmailAddress(smimeEncryptionKey, email)) { msg = i18n( "One of the configured S/MIME encryption certificates does not contain " "the configured email address for this " "identity (%1).", email); err = true; } - if (err && KMessageBox::warningContinueCancel(this, - msg, - i18nc("@title:window", "Email Address Not Found in Key/Certificates"), - KStandardGuiItem::cont(), - KStandardGuiItem::cancel(), - QStringLiteral("warn_email_not_in_certificate")) + if (err + && KMessageBox::warningContinueCancel(this, + msg, + i18nc("@title:window", "Email Address Not Found in Key/Certificates"), + KStandardGuiItem::cont(), + KStandardGuiItem::cancel(), + QStringLiteral("warn_email_not_in_certificate")) != KMessageBox::Continue) { return; } accept(); } bool IdentityDialog::keyMatchesEmailAddress(const GpgME::Key &key, const QString &email_) { if (key.isNull()) { return true; } const QString email = email_.trimmed().toLower(); const auto uids = key.userIDs(); for (const auto &uid : uids) { QString em = QString::fromUtf8(uid.email() ? uid.email() : uid.id()); if (em.isEmpty()) { continue; } if (em[0] == QLatin1Char('<')) { em = em.mid(1, em.length() - 2); } if (em.toLower() == email) { return true; } } return false; } void IdentityDialog::setIdentity(KIdentityManagementCore::Identity &ident) { setWindowTitle(i18nc("@title:window", "Edit Identity \"%1\"", ident.identityName())); // "General" tab: mNameEdit->setText(ident.fullName()); mOrganizationEdit->setText(ident.organization()); mEmailEdit->setText(ident.primaryEmailAddress()); mDictionaryCombo->setCurrentByDictionaryName(ident.dictionary()); // "Cryptography" tab: mPGPSigningKeyRequester->setDefaultKey(QLatin1String(ident.pgpSigningKey())); mPGPEncryptionKeyRequester->setDefaultKey(QLatin1String(ident.pgpEncryptionKey())); mPGPSameKey->setChecked(ident.pgpSigningKey() == ident.pgpEncryptionKey()); mSMIMESigningKeyRequester->setDefaultKey(QLatin1String(ident.smimeSigningKey())); mSMIMEEncryptionKeyRequester->setDefaultKey(QLatin1String(ident.smimeEncryptionKey())); mWarnNotEncrypt->setChecked(ident.warnNotEncrypt()); // "Signature" tab: mSignatureConfigurator->setImageLocation(ident); mSignatureConfigurator->setSignature(ident.signature()); // set the configured email address as initial query of the key // requesters: const QString name = mNameEdit->text().trimmed(); const QString email = mEmailEdit->text().trimmed(); mPGPEncryptionKeyRequester->setIdentity(name, email); mPGPSigningKeyRequester->setIdentity(name, email); mSMIMEEncryptionKeyRequester->setIdentity(name, email); mSMIMESigningKeyRequester->setIdentity(name, email); } void IdentityDialog::updateIdentity(KIdentityManagementCore::Identity &ident) { // "General" tab: ident.setFullName(mNameEdit->text()); ident.setOrganization(mOrganizationEdit->text()); QString email = mEmailEdit->text().trimmed(); ident.setPrimaryEmailAddress(email); // "Cryptography" tab: ident.setPGPSigningKey(mPGPSigningKeyRequester->currentKey().primaryFingerprint()); ident.setPGPEncryptionKey(mPGPEncryptionKeyRequester->currentKey().primaryFingerprint()); ident.setSMIMESigningKey(mSMIMESigningKeyRequester->currentKey().primaryFingerprint()); ident.setSMIMEEncryptionKey(mSMIMEEncryptionKeyRequester->currentKey().primaryFingerprint()); ident.setEncryptionOverride(true); ident.setWarnNotEncrypt(mWarnNotEncrypt->isChecked()); ident.setWarnNotEncrypt(mWarnNotEncrypt->isChecked()); // "Advanced" tab: ident.setDictionary(mDictionaryCombo->currentDictionaryName()); // "Signature" tab: ident.setSignature(mSignatureConfigurator->signature()); } void IdentityDialog::slotKeyListingFinished(KeySelectionCombo *combo) { mInitialLoadingFinished << combo; if (mInitialLoadingFinished.count() == 2) { Q_EMIT keyListingFinished(); } } } #include "identitydialog.moc" #include "moc_identitydialog.cpp" diff --git a/client/identity/signature.h b/client/identity/signature.h index c83c4b8..f544943 100644 --- a/client/identity/signature.h +++ b/client/identity/signature.h @@ -1,206 +1,209 @@ /* SPDX-FileCopyrightText: 2002-2004 Marc Mutz SPDX-FileCopyrightText: 2007 Tom Albers SPDX-FileCopyrightText: 2009 Thomas McGuire Author: Stefan Taferner SPDX-License-Identifier: LGPL-2.0-or-later */ #pragma once #include #include #include #include class KConfigGroup; namespace KIdentityManagementCore { class Signature; class Identity; class SignaturePrivate; QDataStream &operator<<(QDataStream &stream, const KIdentityManagementCore::Signature &sig); QDataStream &operator>>(QDataStream &stream, KIdentityManagementCore::Signature &sig); /** * @short Abstraction of a signature (aka "footer"). * * The signature can either be plain text, HTML text, text returned from a command or text stored * in a file. * * In case of HTML text, the signature can contain images. * Since you set the HTML source with setText(), there also needs to be a way to add the images * to the signature, as the HTML source contains only the img tags that reference those images. * To add the image to the signature, call addImage(). The name given there must match the name * of the img tag in the HTML source. * * The images need to be stored somewhere. The Signature class handles that by storing all images * in a directory. You must set that directory with setImageLocation(), before calling addImage(). * The images added with addImage() are then saved to that directory when calling writeConfig(). * When loading a signature, readConfig() automatically loads the images as well. * To actually add the images to a text edit, call insertIntoTextEdit(). * * Example of creating a HTML signature and then inserting it into a text edit: * @code * Signature htmlSig; * htmlSig.setText( " World" ); * htmlSig.setInlinedHtml( true ); * htmlSig.setImageLocation( KStandardDirs::locateLocal( "data", "emailidentities/example/" ); * QImage image = ...; * htmlSig.addImage( image, "hello.png" ); * ... * KTextEdit edit; * htmlSig.insertIntoTextEdit( KIdentityManagementCore::Signature::End, * KIdentityManagementCore::Signature::AddSeparator, &edit ); * @endcode */ class Signature { friend class Identity; friend QDataStream &operator<<(QDataStream &stream, const Signature &sig); friend QDataStream &operator>>(QDataStream &stream, Signature &sig); public: /** Type of signature (ie. way to obtain the signature text) */ - enum Type { Disabled = 0, Inlined = 1, }; + enum Type { + Disabled = 0, + Inlined = 1, + }; /** * Describes the placement of the signature text when it is to be inserted into a * text edit */ enum Placement { Start, ///< The signature is placed at the start of the textedit End, ///< The signature is placed at the end of the textedit AtCursor ///< The signature is placed at the current cursor position }; struct EmbeddedImage { QImage image; QString name; }; using EmbeddedImagePtr = QSharedPointer; /** Used for comparison */ bool operator==(const Signature &other) const; /** Constructor for disabled signature */ Signature(); /** Constructor for inline text */ Signature(const QString &text); /** Copy constructor */ Signature(const Signature &that); /** Assignment operator */ Signature &operator=(const Signature &that); /** Destructor */ ~Signature(); /** @return the raw signature text as entered resp. read from file. @param ok set to @c true if reading succeeded @param errorMessage If available, contains a human readable explanation for @p ok being @c false. */ [[nodiscard]] QString rawText(bool *ok = nullptr, QString *errorMessage = nullptr) const; /** @return the signature text with a "-- \n" separator added, if necessary. A newline will not be appended or prepended. @param ok set to @c true if reading succeeded @param errorMessage If available, contains a human readable explanation for @p ok being @c false. */ [[nodiscard]] QString withSeparator(bool *ok = nullptr, QString *errorMessage = nullptr) const; /** Set the signature text and mark this signature as being of "inline text" type. */ void setText(const QString &text); [[nodiscard]] QString text() const; /** * Returns the text of the signature. If the signature is HTML, the HTML * tags will be stripped. * @since 4.4 */ [[nodiscard]] QString toPlainText() const; /// @return the type of signature (ie. way to obtain the signature text) [[nodiscard]] Type type() const; void setType(Type type); /** * Sets the inlined signature to text or html * @param isHtml sets the inlined signature to html * @since 4.1 */ void setInlinedHtml(bool isHtml); /** * @return boolean whether the inlined signature is html * @since 4.1 */ [[nodiscard]] bool isInlinedHtml() const; /** * Sets the location where the copies of the signature images will be stored. * The images will be stored there when calling writeConfig(). The image location * is stored in the config, so the next readConfig() call knows where to look for * images. * It is recommended to use KStandardDirs::locateLocal( "data", "emailidentities/%1" ) * for the location, where %1 is the unique identifier of the identity. * * @warning readConfig will delete all other PNG files in this directory, as they could * be stale inline image files * * Like with addImage(), the SignatureConfigurator will handle this for you. * @param path the path to set as image location * @since 4.4 */ void setImageLocation(const QString &path); [[nodiscard]] QString imageLocation() const; /** * Adds the given image to the signature. * This is needed if you use setText() to set some HTML source that references images. Those * referenced images needed to be added by calling this function. The @imageName has to match * the src attribute of the img tag. * * If you use SignatureConfigurator, you don't need to call this function, as the configurator * will handle this for you. * setImageLocation() needs to be called once before. * @since 4.4 */ void addImage(const QImage &image, const QString &imageName); /** * @brief setEnabledSignature * @param enabled enables signature if set as @c true * @since 4.9 */ void setEnabledSignature(bool enabled); [[nodiscard]] bool isEnabledSignature() const; enum AddedTextFlag { AddNothing = 0, ///< Don't add any text to the signature AddSeparator = 1 << 0, ///< The separator '-- \n' will be added in front /// of the signature AddNewLines = 1 << 1 ///< Add a newline character in front or after the signature, /// depending on the placement }; /// Describes which additional parts should be added to the signature using AddedText = QFlags; [[nodiscard]] QList embeddedImages() const; void setEmbeddedImages(const QList &embedded); protected: // TODO: KDE5: BIC: Move all to private class void writeConfig(KConfigGroup &config) const; void readConfig(const KConfigGroup &config); private: //@cond PRIVATE std::unique_ptr const d; //@endcond }; Q_DECLARE_OPERATORS_FOR_FLAGS(Signature::AddedText) } diff --git a/client/main.cpp b/client/main.cpp index c7e65fe..4b64107 100644 --- a/client/main.cpp +++ b/client/main.cpp @@ -1,52 +1,52 @@ // SPDX-FileCopyrightText: 2023 g10 code Gmbh // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "controllers/emailcontroller.h" +#include +#include +#include #include #include -#include +#include #include #include -#include -#include -#include #include -#include -#include +#include +#include -#include "websocketclient.h" -#include "webserver.h" #include "qnam.h" +#include "webserver.h" +#include "websocketclient.h" using namespace Qt::Literals::StringLiterals; using namespace std::chrono; int main(int argc, char *argv[]) { QApplication app(argc, argv); app.setQuitOnLastWindowClosed(false); KLocalizedString::setApplicationDomain(QByteArrayLiteral("gpgol")); QObject::connect(qnam, &QNetworkAccessManager::sslErrors, qnam, [](QNetworkReply *reply, const QList &) { reply->ignoreSslErrors(); }); const auto clientId = QUuid::createUuid().toString(QUuid::WithoutBraces); WebServer server; server.run(); if (!server.running()) { qWarning() << "Server failed to listen on a port."; return 1; } const auto port = server.port(); WebsocketClient::self(QUrl(u"wss://localhost:5657/"_s), port, clientId); auto keyCache = Kleo::KeyCache::mutableInstance(); keyCache->startKeyListing(); return app.exec(); } diff --git a/client/websocketclient.cpp b/client/websocketclient.cpp index 23c3a78..eaec5b5 100644 --- a/client/websocketclient.cpp +++ b/client/websocketclient.cpp @@ -1,190 +1,186 @@ // SPDX-FileCopyrightText: 2023 g10 code Gmbh // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "websocketclient.h" // Qt headers -#include +#include #include #include -#include +#include #include #include -#include #include +#include // KDE headers #include #include // gpgme headers #include -#include "websocket_debug.h" #include "qnam.h" +#include "websocket_debug.h" using namespace std::chrono; using namespace Qt::Literals::StringLiterals; -namespace { +namespace +{ QStringList trustedEmails(const std::shared_ptr &keyCache) { QStringList emails; const auto keys = keyCache->keys(); for (const auto &key : keys) { for (const auto &userId : key.userIDs()) { if (key.ownerTrust() == GpgME::Key::Ultimate) { emails << QString::fromLatin1(userId.email()).toLower(); break; } } } return emails; } auto delay = 2000ms; void registerServer(int port, const QStringList &emails, const QString &clientId) { QNetworkRequest registerRequest(QUrl(u"https://127.0.0.1:5656/register"_s)); registerRequest.setHeader(QNetworkRequest::ContentTypeHeader, u"application/json"_s); - auto registerReply = qnam->post(registerRequest, QJsonDocument(QJsonObject{ - { "port"_L1, port }, - { "emails"_L1, QJsonArray::fromStringList(emails) }, - { "clientId"_L1, clientId }, - }).toJson()); - + auto registerReply = qnam->post(registerRequest, + QJsonDocument(QJsonObject{ + {"port"_L1, port}, + {"emails"_L1, QJsonArray::fromStringList(emails)}, + {"clientId"_L1, clientId}, + }) + .toJson()); QObject::connect(registerReply, &QNetworkReply::finished, qnam, [port, emails, clientId, registerReply]() { if (registerReply->error() != QNetworkReply::NoError) { QTimer::singleShot(delay, [emails, port, clientId]() { delay *= 2; registerServer(port, emails, clientId); }); qWarning() << "Failed to register" << registerReply->errorString() << "retrying in" << delay; } else { qWarning() << "Register"; } registerReply->deleteLater(); }); } } WebsocketClient &WebsocketClient::self(const QUrl &url, int port, const QString &clientId) { static WebsocketClient *client = nullptr; if (!client && url.isEmpty()) { qFatal() << "Unable to create a client without an url"; } else if (!client) { client = new WebsocketClient(url, port, clientId); } return *client; }; WebsocketClient::WebsocketClient(const QUrl &url, int port, const QString &clientId) : QObject(nullptr) , m_webSocket(QWebSocket(QStringLiteral("Client"))) , m_url(url) , m_port(port) , m_clientId(clientId) { connect(&m_webSocket, &QWebSocket::connected, this, &WebsocketClient::slotConnected); connect(&m_webSocket, &QWebSocket::disconnected, this, [this] { Q_EMIT closed(i18nc("@info", "Connection to outlook lost due to a disconnection with the broker.")); }); connect(&m_webSocket, &QWebSocket::errorOccurred, this, &WebsocketClient::slotErrorOccurred); connect(&m_webSocket, &QWebSocket::textMessageReceived, this, &WebsocketClient::slotTextMessageReceived); - connect(&m_webSocket, QOverload&>::of(&QWebSocket::sslErrors), this, [this](const QList &errors) { + connect(&m_webSocket, QOverload &>::of(&QWebSocket::sslErrors), this, [this](const QList &errors) { // TODO remove m_webSocket.ignoreSslErrors(errors); }); QSslConfiguration sslConfiguration; auto certPath = QStandardPaths::locate(QStandardPaths::AppLocalDataLocation, QStringLiteral("certificate.pem")); Q_ASSERT(!certPath.isEmpty()); QFile certFile(certPath); if (!certFile.open(QIODevice::ReadOnly)) { qFatal() << "Couldn't read certificate"; } QSslCertificate certificate(&certFile, QSsl::Pem); certFile.close(); sslConfiguration.addCaCertificate(certificate); m_webSocket.setSslConfiguration(sslConfiguration); m_webSocket.open(url); auto globalKeyCache = Kleo::KeyCache::instance(); m_emails = trustedEmails(globalKeyCache); if (m_emails.isEmpty()) { qWarning() << "No ultimately keys found in keychain"; return; } // TODO remove me QObject::connect(qnam, &QNetworkAccessManager::sslErrors, qnam, [](QNetworkReply *reply, const QList &errors) { reply->ignoreSslErrors(); }); registerServer(m_port, m_emails, m_clientId); } void WebsocketClient::slotConnected() { qCInfo(WEBSOCKET_LOG) << "websocket connected"; - QJsonDocument doc(QJsonObject{ - { "command"_L1, "register"_L1 }, - { "arguments"_L1, QJsonObject { - { "emails"_L1, QJsonArray::fromStringList(m_emails) }, - { "type"_L1, "nativeclient"_L1 } - }} - }); + QJsonDocument doc(QJsonObject{{"command"_L1, "register"_L1}, + {"arguments"_L1, QJsonObject{{"emails"_L1, QJsonArray::fromStringList(m_emails)}, {"type"_L1, "nativeclient"_L1}}}}); m_webSocket.sendTextMessage(QString::fromUtf8(doc.toJson())); registerServer(m_port, m_emails, m_clientId); Q_EMIT connected(); } void WebsocketClient::slotErrorOccurred(QAbstractSocket::SocketError error) { qCWarning(WEBSOCKET_LOG) << error << m_webSocket.errorString(); Q_EMIT closed(i18nc("@info", "Connection to outlook lost due to a connection error.")); reconnect(); } void WebsocketClient::slotTextMessageReceived(QString message) { const auto doc = QJsonDocument::fromJson(message.toUtf8()); if (!doc.isObject()) { qCWarning(WEBSOCKET_LOG) << "invalid text message received" << message; return; } - const auto object = doc.object(); qCDebug(WEBSOCKET_LOG) << object; if (object["type"_L1] == "disconnection"_L1) { // disconnection of the web client Q_EMIT closed(i18nc("@info", "Connection to outlook lost. Make sure the extension tab is open.")); } else if (object["type"_L1] == "connection"_L1) { // reconnection of the web client Q_EMIT connected(); } else if (object["type"_L1] == "email-sent"_L1) { // confirmation that the email was sent const auto args = object["arguments"_L1].toObject(); Q_EMIT emailSentSuccessfully(args["id"_L1].toString()); } } void WebsocketClient::reconnect() { QTimer::singleShot(1000ms, this, [this]() { m_webSocket.open(m_url); }); } diff --git a/server/controllers/abstractcontroller.cpp b/server/controllers/abstractcontroller.cpp index 0bab60d..b0f0ed5 100644 --- a/server/controllers/abstractcontroller.cpp +++ b/server/controllers/abstractcontroller.cpp @@ -1,47 +1,41 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "abstractcontroller.h" #include "../model/serverstate.h" #include "http_debug.h" -#include #include +#include #include using namespace Qt::Literals::StringLiterals; QHttpServerResponse AbstractController::badRequest(const QString &reason) { if (reason.isEmpty()) { - return QHttpServerResponse(QJsonObject { - { "errorMessage"_L1, "Invalid request"_L1 } - }, QHttpServerResponse::StatusCode::BadRequest); + return QHttpServerResponse(QJsonObject{{"errorMessage"_L1, "Invalid request"_L1}}, QHttpServerResponse::StatusCode::BadRequest); } else { - return QHttpServerResponse(QJsonObject { - { "errorMessage"_L1, QJsonValue("Invalid request: "_L1 + reason) } - }, QHttpServerResponse::StatusCode::BadRequest); + return QHttpServerResponse(QJsonObject{{"errorMessage"_L1, QJsonValue("Invalid request: "_L1 + reason)}}, QHttpServerResponse::StatusCode::BadRequest); } } QHttpServerResponse AbstractController::forbidden() { - return QHttpServerResponse(QJsonObject { - { "errorMessage"_L1, "Unable to authentificate"_L1 } - }, QHttpServerResponse::StatusCode::Forbidden); + return QHttpServerResponse(QJsonObject{{"errorMessage"_L1, "Unable to authentificate"_L1}}, QHttpServerResponse::StatusCode::Forbidden); } std::optional AbstractController::checkAuthentification(const QHttpServerRequest &request) { const auto &state = ServerState::instance(); const auto email = QString::fromUtf8(Utils::findHeader(request.headers(), "X-EMAIL")); if (email.isEmpty() || !state.clients.contains(email)) { qCWarning(HTTP_LOG) << "no email found" << email; return std::nullopt; } return state.clients[email]; } diff --git a/server/controllers/draftcontroller.h b/server/controllers/draftcontroller.h index 7d65a75..2a4d1a7 100644 --- a/server/controllers/draftcontroller.h +++ b/server/controllers/draftcontroller.h @@ -1,16 +1,16 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "abstractcontroller.h" #include class DraftController : public AbstractController { public: static QHttpServerResponse listAction(const QHttpServerRequest &request); - static QHttpServerResponse getAction(const QHttpServerRequest &request, QString ); + static QHttpServerResponse getAction(const QHttpServerRequest &request, QString); }; diff --git a/server/controllers/emailcontroller.cpp b/server/controllers/emailcontroller.cpp index 3bff3b1..1b1f6ea 100644 --- a/server/controllers/emailcontroller.cpp +++ b/server/controllers/emailcontroller.cpp @@ -1,162 +1,160 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "emailcontroller.h" -#include +#include #include -#include +#include +#include #include +#include #include -#include -#include #include #include -#include "webserver.h" #include "http_debug.h" +#include "webserver.h" using namespace Qt::Literals::StringLiterals; QHttpServerResponse EmailController::abstractEmailAction(const QHttpServerRequest &request, const QString &action, QHttpServerRequest::Method method) { - const auto client = checkAuthentification(request); if (!client) { return forbidden(); } QNetworkRequest viewEmailRequest(QUrl(u"http://127.0.0.1:"_s + QString::number(client->port) + u'/' + action)); viewEmailRequest.setHeader(QNetworkRequest::ContentTypeHeader, u"application/json"_s); auto email = Utils::findHeader(request.headers(), "X-EMAIL"); auto displayName = Utils::findHeader(request.headers(), "X-NAME"); auto token = QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8(); viewEmailRequest.setRawHeader("X-EMAIL", email); viewEmailRequest.setRawHeader("X-TOKEN", token); viewEmailRequest.setRawHeader("X-NAME", displayName); auto &serverState = ServerState::instance(); serverState.composerRequest[token] = QString::fromUtf8(email); - auto &state = ServerState::instance(); QNetworkReply *reply; if (method == QHttpServerRequest::Method::Post) { const auto body = request.body(); reply = state.qnam.post(viewEmailRequest, body); } else { reply = state.qnam.deleteResource(viewEmailRequest); } QObject::connect(reply, &QNetworkReply::finished, reply, [reply]() { if (reply->error() != QNetworkReply::NoError) { qCWarning(HTTP_LOG) << reply->error() << reply->errorString(); } }); - return QHttpServerResponse(QJsonObject { + return QHttpServerResponse(QJsonObject{ {"status"_L1, "ok"_L1}, }); } QHttpServerResponse EmailController::viewEmailAction(const QHttpServerRequest &request) { if (request.method() != QHttpServerRequest::Method::Post) { return badRequest(u"Endpoint only supports POST request"_s); } return abstractEmailAction(request, u"view"_s); } QHttpServerResponse EmailController::newEmailAction(const QHttpServerRequest &request) { if (request.method() != QHttpServerRequest::Method::Post) { return badRequest(u"Endpoint only supports POST request"_s); } return abstractEmailAction(request, u"new"_s); } QHttpServerResponse EmailController::draftAction(QString, const QHttpServerRequest &request) { if (request.method() != QHttpServerRequest::Method::Post && request.method() != QHttpServerRequest::Method::Delete) { return badRequest(u"Endpoint only supports POST request"_s); } return abstractEmailAction(request, request.url().path(), request.method()); } QHttpServerResponse EmailController::replyEmailAction(const QHttpServerRequest &request) { if (request.method() != QHttpServerRequest::Method::Post) { return badRequest(u"Endpoint only supports POST request"_s); } return abstractEmailAction(request, u"reply"_s); } QHttpServerResponse EmailController::forwardEmailAction(const QHttpServerRequest &request) { if (request.method() != QHttpServerRequest::Method::Post) { return badRequest(u"Endpoint only supports POST request"_s); } return abstractEmailAction(request, u"forward"_s); } QHttpServerResponse checkStatus(int port, const QByteArray &body) { QNetworkRequest infoEmailRequest(QUrl(u"http://127.0.0.1:"_s + QString::number(port) + u"/info"_s)); infoEmailRequest.setHeader(QNetworkRequest::ContentTypeHeader, u"application/json"_s); auto &state = ServerState::instance(); QEventLoop eventLoop; auto reply = state.qnam.post(infoEmailRequest, body); QObject::connect(reply, &QNetworkReply::finished, &eventLoop, &QEventLoop::quit); eventLoop.exec(); QJsonParseError error; const auto resultBody = QJsonDocument::fromJson(reply->readAll(), &error); if (resultBody.isNull()) { return QHttpServerResponse(QHttpServerResponse::StatusCode::BadRequest); } if (!resultBody.isObject()) { return QHttpServerResponse(QHttpServerResponse::StatusCode::BadRequest); } return QHttpServerResponse{resultBody.object()}; } QHttpServerResponse EmailController::infoEmailAction(const QHttpServerRequest &request) { if (request.method() != QHttpServerRequest::Method::Post) { return badRequest(u"Endpoint only supports POST request"_s); } const auto server = checkAuthentification(request); if (!server) { return forbidden(); } return checkStatus(server->port, request.body()); } QHttpServerResponse EmailController::socketWebAction(const QHttpServerRequest &request) { const auto email = QString::fromUtf8(Utils::findHeader(request.headers(), "X-EMAIL")); const auto token = Utils::findHeader(request.headers(), "X-TOKEN"); const auto &serverState = ServerState::instance(); qDebug() << serverState.composerRequest << email << token; if (serverState.composerRequest[token] != email) { return forbidden(); } WebServer::self().sendMessageToWebClient(email, request.body()); return QHttpServerResponse(QJsonObject{ - { "status"_L1, "OK"_L1 }, + {"status"_L1, "OK"_L1}, }); } diff --git a/server/controllers/emailcontroller.h b/server/controllers/emailcontroller.h index ead4a20..08d1196 100644 --- a/server/controllers/emailcontroller.h +++ b/server/controllers/emailcontroller.h @@ -1,27 +1,28 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "abstractcontroller.h" #include class EmailController : public AbstractController { public: // Request from the web client static QHttpServerResponse viewEmailAction(const QHttpServerRequest &request); static QHttpServerResponse infoEmailAction(const QHttpServerRequest &request); static QHttpServerResponse newEmailAction(const QHttpServerRequest &request); static QHttpServerResponse replyEmailAction(const QHttpServerRequest &request); static QHttpServerResponse forwardEmailAction(const QHttpServerRequest &request); static QHttpServerResponse draftAction(QString draft, const QHttpServerRequest &request); /// Forward request from the native client to the web client static QHttpServerResponse socketWebAction(const QHttpServerRequest &request); private: - static QHttpServerResponse abstractEmailAction(const QHttpServerRequest &request, const QString &action, QHttpServerRequest::Method method = QHttpServerRequest::Method::Post); + static QHttpServerResponse + abstractEmailAction(const QHttpServerRequest &request, const QString &action, QHttpServerRequest::Method method = QHttpServerRequest::Method::Post); }; diff --git a/server/controllers/registrationcontroller.cpp b/server/controllers/registrationcontroller.cpp index ed3e4e0..9459f34 100644 --- a/server/controllers/registrationcontroller.cpp +++ b/server/controllers/registrationcontroller.cpp @@ -1,45 +1,45 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "registrationcontroller.h" +#include +#include #include #include -#include -#include #include -#include +#include #include "../model/serverstate.h" #include "http_debug.h" using namespace Qt::Literals::StringLiterals; QHttpServerResponse RegistrationController::registerAction(const QHttpServerRequest &request) { const auto json = QJsonDocument::fromJson(request.body()); if (json.isEmpty() || !json.isObject()) { return badRequest(); } const auto object = json.object(); if (object.isEmpty() || !object.contains("port"_L1) || !object.contains("emails"_L1)) { return badRequest(); } const auto emails = object["emails"_L1].toArray(); const Client::Id clientId = object["clientId"_L1].toString(); const auto port = object["port"_L1].toInt(); for (const auto email : emails) { - ServerState::instance().clients[email.toString().toLower()] = Client { + ServerState::instance().clients[email.toString().toLower()] = Client{ clientId, port, }; qCInfo(HTTP_LOG) << "Registration of email" << email.toString() << "on port" << object["port"_L1].toInt() << "with clientId" << clientId; } return QHttpServerResponse("text/plain", "Server added\n"); } diff --git a/server/controllers/registrationcontroller.h b/server/controllers/registrationcontroller.h index c91c9ee..a17b1e1 100644 --- a/server/controllers/registrationcontroller.h +++ b/server/controllers/registrationcontroller.h @@ -1,16 +1,16 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "abstractcontroller.h" -#include #include +#include class RegistrationController : public AbstractController { public: static QHttpServerResponse registerAction(const QHttpServerRequest &request); }; diff --git a/server/controllers/staticcontroller.cpp b/server/controllers/staticcontroller.cpp index 2e60752..ff8cb8b 100644 --- a/server/controllers/staticcontroller.cpp +++ b/server/controllers/staticcontroller.cpp @@ -1,40 +1,40 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "staticcontroller.h" -#include -#include #include "http_debug.h" +#include +#include using namespace Qt::Literals::StringLiterals; QHttpServerResponse StaticController::homeAction(const QHttpServerRequest &) { QFile file(u":/web/index.html"_s); if (!file.open(QIODeviceBase::ReadOnly)) { qCWarning(HTTP_LOG) << file.errorString(); return QHttpServerResponse(QHttpServerResponder::StatusCode::NotFound); } return QHttpServerResponse("text/html", file.readAll()); } QHttpServerResponse StaticController::assetsAction(QString fileName, const QHttpServerRequest &) { QFile file(u":/web/assets/"_s + fileName); if (!file.open(QIODeviceBase::ReadOnly)) { qCWarning(HTTP_LOG) << file.errorString(); return QHttpServerResponse(QHttpServerResponder::StatusCode::NotFound); } if (fileName.endsWith(u".png"_s)) { return QHttpServerResponse("image/png", file.readAll()); } else if (fileName.endsWith(u".js"_s)) { return QHttpServerResponse("text/javascript", file.readAll()); } else if (fileName.endsWith(u".css"_s)) { return QHttpServerResponse("text/css", file.readAll()); } return QHttpServerResponse("text/plain", file.readAll()); } diff --git a/server/main.cpp b/server/main.cpp index 29eb997..bf086f2 100644 --- a/server/main.cpp +++ b/server/main.cpp @@ -1,31 +1,31 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include #include #include #include #include #include #include #include -#include "webserver.h" #include "http_debug.h" +#include "webserver.h" using namespace Qt::Literals::StringLiterals; int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); auto &webServer = WebServer::self(); if (!webServer.run()) { qCWarning(HTTP_LOG) << "Server failed to listen on a port."; return 1; } return app.exec(); } diff --git a/server/model/serverstate.cpp b/server/model/serverstate.cpp index f3601fa..4a12ab0 100644 --- a/server/model/serverstate.cpp +++ b/server/model/serverstate.cpp @@ -1,14 +1,15 @@ // SPDX-FileCopyrightText: 2023 g10 code Gmbh // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "serverstate.h" ServerState::ServerState() -{} +{ +} ServerState &ServerState::instance() { static ServerState s; return s; } diff --git a/server/model/serverstate.h b/server/model/serverstate.h index 8701691..985c8f9 100644 --- a/server/model/serverstate.h +++ b/server/model/serverstate.h @@ -1,35 +1,35 @@ // SPDX-FileCopyrightText: 2023 g10 code Gmbh // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include #include #include +#include using Token = QByteArray; using ClientId = QString; using Email = QString; using Port = int; struct Client { using Id = QString; Id id; Port port; }; class ServerState { public: static ServerState &instance(); QHash clients; QNetworkAccessManager qnam; QHash composerRequest; private: ServerState(); }; diff --git a/server/webserver.cpp b/server/webserver.cpp index 75deca3..cd89486 100644 --- a/server/webserver.cpp +++ b/server/webserver.cpp @@ -1,302 +1,287 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "webserver.h" #include #include -#include -#include -#include -#include #include #include +#include +#include +#include #include #include -#include -#include #include +#include +#include +#include "controllers/emailcontroller.h" #include "controllers/registrationcontroller.h" #include "controllers/staticcontroller.h" -#include "controllers/emailcontroller.h" -#include "websocket_debug.h" #include "http_debug.h" +#include "websocket_debug.h" using namespace Qt::Literals::StringLiterals; WebServer WebServer::s_instance = WebServer(); WebServer &WebServer::self() { return s_instance; } WebServer::WebServer() : QObject(nullptr) , m_httpServer(new QHttpServer(this)) , m_webSocketServer(new QWebSocketServer(u"GPGOL"_s, QWebSocketServer::SslMode::SecureMode, this)) { } WebServer::~WebServer() = default; bool WebServer::run() { auto keyPath = QStandardPaths::locate(QStandardPaths::AppLocalDataLocation, QStringLiteral("certificate-key.pem")); auto certPath = QStandardPaths::locate(QStandardPaths::AppLocalDataLocation, QStringLiteral("certificate.pem")); Q_ASSERT(!keyPath.isEmpty()); Q_ASSERT(!certPath.isEmpty()); QFile privateKeyFile(keyPath); if (!privateKeyFile.open(QIODevice::ReadOnly)) { qCFatal(HTTP_LOG) << u"Couldn't open file for reading: %1"_s.arg(privateKeyFile.errorString()); return false; } const QSslKey sslKey(&privateKeyFile, QSsl::Rsa); privateKeyFile.close(); const auto sslCertificateChain = QSslCertificate::fromPath(certPath); if (sslCertificateChain.isEmpty()) { qCFatal(HTTP_LOG) << u"Couldn't retrieve SSL certificate from file."_s; return false; } // Static assets controller m_httpServer->route(u"/home"_s, &StaticController::homeAction); m_httpServer->route(u"/assets/"_s, &StaticController::assetsAction); // Registration controller m_httpServer->route(u"/register"_s, &RegistrationController::registerAction); // Email controller m_httpServer->route(u"/view"_s, &EmailController::viewEmailAction); m_httpServer->route(u"/info"_s, &EmailController::infoEmailAction); m_httpServer->route(u"/reply"_s, &EmailController::replyEmailAction); m_httpServer->route(u"/forward"_s, &EmailController::forwardEmailAction); m_httpServer->route(u"/new"_s, &EmailController::newEmailAction); m_httpServer->route(u"/socket-web"_s, &EmailController::socketWebAction); m_httpServer->route(u"/draft/"_s, &EmailController::draftAction); m_httpServer->sslSetup(sslCertificateChain.front(), sslKey); const auto port = m_httpServer->listen(QHostAddress::Any, WebServer::Port); if (!port) { qCFatal(HTTP_LOG) << "Server failed to listen on a port."; return false; } qInfo(HTTP_LOG) << u"Running http server on https://127.0.0.1:%1/ (Press CTRL+C to quit)"_s.arg(port); - QSslConfiguration sslConfiguration; sslConfiguration.setPeerVerifyMode(QSslSocket::VerifyNone); sslConfiguration.setLocalCertificate(sslCertificateChain.front()); sslConfiguration.setPrivateKey(sslKey); m_webSocketServer->setSslConfiguration(sslConfiguration); if (m_webSocketServer->listen(QHostAddress::Any, WebServer::WebSocketPort)) { qCInfo(WEBSOCKET_LOG) << u"Running websocket server on wss://127.0.0.1:%1/ (Press CTRL+C to quit)"_s.arg(WebServer::Port + 1); connect(m_webSocketServer, &QWebSocketServer::newConnection, this, &WebServer::onNewConnection); } - return true; } void WebServer::onNewConnection() { auto pSocket = m_webSocketServer->nextPendingConnection(); if (!pSocket) { return; } qCInfo(WEBSOCKET_LOG) << "Client connected:" << pSocket->peerName() << pSocket->origin() << pSocket->localAddress() << pSocket->localPort(); connect(pSocket, &QWebSocket::textMessageReceived, this, &WebServer::processTextMessage); - connect(pSocket, &QWebSocket::binaryMessageReceived, - this, &WebServer::processBinaryMessage); + connect(pSocket, &QWebSocket::binaryMessageReceived, this, &WebServer::processBinaryMessage); connect(pSocket, &QWebSocket::disconnected, this, &WebServer::socketDisconnected); m_clients << pSocket; } void WebServer::processTextMessage(QString message) { auto webClient = qobject_cast(sender()); if (webClient) { QJsonParseError error; const auto doc = QJsonDocument::fromJson(message.toUtf8(), &error); if (error.error != QJsonParseError::NoError) { qCWarning(WEBSOCKET_LOG) << "Error parsing json" << error.errorString(); return; } if (!doc.isObject()) { qCWarning(WEBSOCKET_LOG) << "Invalid json received"; return; } const auto object = doc.object(); - if (!object.contains("command"_L1) || !object["command"_L1].isString() - || !object.contains("arguments"_L1) || !object["arguments"_L1].isObject()) { - qCWarning(WEBSOCKET_LOG) << "Invalid json received: no command or arguments set" ; + if (!object.contains("command"_L1) || !object["command"_L1].isString() || !object.contains("arguments"_L1) || !object["arguments"_L1].isObject()) { + qCWarning(WEBSOCKET_LOG) << "Invalid json received: no command or arguments set"; return; } - static QHash commandMapping { - { "register"_L1, WebServer::Command::Register }, - { "email-sent"_L1, WebServer::Command::EmailSent }, + static QHash commandMapping{ + {"register"_L1, WebServer::Command::Register}, + {"email-sent"_L1, WebServer::Command::EmailSent}, }; const auto command = commandMapping[doc["command"_L1].toString()]; processCommand(command, object["arguments"_L1].toObject(), webClient); } } void WebServer::processCommand(Command command, const QJsonObject &arguments, QWebSocket *socket) { switch (command) { case Command::Register: { const auto type = arguments["type"_L1].toString(); qCWarning(WEBSOCKET_LOG) << "Register" << arguments; if (type.isEmpty()) { qCWarning(WEBSOCKET_LOG) << "empty client type given when registering"; return; } const auto emails = arguments["emails"_L1].toArray(); if (type == "webclient"_L1) { if (emails.isEmpty()) { qCWarning(WEBSOCKET_LOG) << "empty email given"; } for (const auto &email : emails) { m_webClientsMappingToEmail[email.toString()] = socket; qCWarning(WEBSOCKET_LOG) << "email" << email.toString() << "mapped to a web client"; const auto nativeClient = m_nativeClientsMappingToEmail[email.toString()]; if (nativeClient) { - QJsonDocument doc(QJsonObject{ - { "type"_L1, "connection"_L1 }, - { "payload"_L1, QJsonObject{ - { "client_type"_L1, "web_client"_L1 } - }} - }); + QJsonDocument doc(QJsonObject{{"type"_L1, "connection"_L1}, {"payload"_L1, QJsonObject{{"client_type"_L1, "web_client"_L1}}}}); nativeClient->sendTextMessage(QString::fromUtf8(doc.toJson())); } } } else { if (emails.isEmpty()) { qCWarning(WEBSOCKET_LOG) << "empty email given"; } for (const auto &email : emails) { m_nativeClientsMappingToEmail[email.toString()] = socket; qCWarning(WEBSOCKET_LOG) << "email" << email.toString() << "mapped to a native client"; const auto webClient = m_webClientsMappingToEmail[email.toString()]; if (webClient) { - QJsonDocument doc(QJsonObject{ - { "type"_L1, "connection"_L1 }, - { "payload"_L1, QJsonObject{ - { "client_type"_L1, "native_client"_L1 } - }} - }); + QJsonDocument doc(QJsonObject{{"type"_L1, "connection"_L1}, {"payload"_L1, QJsonObject{{"client_type"_L1, "native_client"_L1}}}}); webClient->sendTextMessage(QString::fromUtf8(doc.toJson())); } } } return; } case Command::EmailSent: { const auto email = arguments["email"_L1].toString(); const auto socket = m_nativeClientsMappingToEmail[email]; if (!socket) { return; } QJsonDocument doc(QJsonObject{ - { "type"_L1, "email-sent"_L1 }, - { "arguments"_L1, arguments }, + {"type"_L1, "email-sent"_L1}, + {"arguments"_L1, arguments}, }); socket->sendTextMessage(QString::fromUtf8(doc.toJson())); return; } case Command::Undefined: - qCWarning(WEBSOCKET_LOG) << "Invalid json received: invalid command" ; + qCWarning(WEBSOCKET_LOG) << "Invalid json received: invalid command"; return; } } bool WebServer::sendMessageToWebClient(const QString &email, const QByteArray &payload) { auto socket = m_webClientsMappingToEmail[email]; if (!socket) { return false; } socket->sendTextMessage(QString::fromUtf8(payload)); return true; } void WebServer::processBinaryMessage(QByteArray message) { qCWarning(WEBSOCKET_LOG) << "got binary message" << message; QWebSocket *pClient = qobject_cast(sender()); if (pClient) { pClient->sendBinaryMessage(message); } } void WebServer::socketDisconnected() { QWebSocket *pClient = qobject_cast(sender()); if (pClient) { qCWarning(WEBSOCKET_LOG) << "Client disconnected" << pClient; // Web client was disconnected { const auto it = std::find_if(m_webClientsMappingToEmail.cbegin(), m_webClientsMappingToEmail.cend(), [pClient](QWebSocket *webSocket) { return pClient == webSocket; }); if (it != m_webClientsMappingToEmail.cend()) { const auto email = it.key(); const auto nativeClient = m_nativeClientsMappingToEmail[email]; qCInfo(WEBSOCKET_LOG) << "webclient with email disconnected" << email << nativeClient; if (nativeClient) { QJsonDocument doc(QJsonObject{ - { "type"_L1, "disconnection"_L1 }, + {"type"_L1, "disconnection"_L1}, }); nativeClient->sendTextMessage(QString::fromUtf8(doc.toJson())); } m_webClientsMappingToEmail.removeIf([pClient](auto socket) { return pClient == socket.value(); }); } } // Native client was disconnected const auto emails = m_nativeClientsMappingToEmail.keys(); for (const auto &email : emails) { const auto webSocket = m_nativeClientsMappingToEmail[email]; if (webSocket != pClient) { qCWarning(WEBSOCKET_LOG) << "webSocket not equal" << email << webSocket << pClient; continue; } qCInfo(WEBSOCKET_LOG) << "native client for" << email << "was disconnected."; QJsonDocument doc(QJsonObject{ - { "type"_L1, "disconnection"_L1 }, + {"type"_L1, "disconnection"_L1}, }); sendMessageToWebClient(email, doc.toJson()); } m_nativeClientsMappingToEmail.removeIf([pClient](auto socket) { return pClient == socket.value(); }); m_clients.removeAll(pClient); } } diff --git a/server/webserver.h b/server/webserver.h index 98db50e..887ed98 100644 --- a/server/webserver.h +++ b/server/webserver.h @@ -1,67 +1,66 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include -#include #include +#include +#include #include QT_FORWARD_DECLARE_CLASS(QWebSocketServer) QT_FORWARD_DECLARE_CLASS(QWebSocket) class WebsocketRequestBackend; class QHttpServer; class WebServer : public QObject { Q_OBJECT public: /// Get singleton instance of WebServer static WebServer &self(); /// WebServer destructor. ~WebServer() override; /// Start web server. bool run(); /// is a valid WebServer instance bool isValid() const; bool sendMessageToWebClient(const QString &email, const QByteArray &payload); bool sendMessageToNativeClient(const QString &email, const QByteArray &payload); private Q_SLOTS: void onNewConnection(); void processTextMessage(QString message); void processBinaryMessage(QByteArray message); void socketDisconnected(); private: WebServer(); enum class Command { Undefined, ///< Undefined command. Register, ///< Registration of a client (native or web). EmailSent, ///< Confirmation that an email was sent. }; enum SpecialValues { Port = 5656, WebSocketPort = 5657, }; void processCommand(Command command, const QJsonObject &arguments, QWebSocket *socket); - QHttpServer * const m_httpServer; - QWebSocketServer * const m_webSocketServer; + QHttpServer *const m_httpServer; + QWebSocketServer *const m_webSocketServer; QList m_clients; QHash m_webClientsMappingToEmail; QHash m_nativeClientsMappingToEmail; static WebServer s_instance; }; -