diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 517344e..987f39b 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -1,270 +1,272 @@ # SPDX-FileCopyrightText: 2023 g10 code GmbH # SPDX-Contributor: Carl Schwan # SPDX-License-Identifier: BSD-2-Clause add_library(gpgol-server-static) target_sources(gpgol-server-static PUBLIC + composerwindowfactory.cpp + composerwindowfactory.h websocketclient.cpp websocketclient.h # Identity identity/addressvalidationjob.cpp identity/addressvalidationjob.h identity/identitycombo.cpp identity/identitycombo.h identity/identitymanager.cpp identity/identitymanager.h identity/identitydialog.cpp identity/identitydialog.h identity/identityaddvcarddialog.cpp identity/identityaddvcarddialog.h # HTTP Controller controllers/emailcontroller.cpp controllers/emailcontroller.h # Draft draft/draft.cpp draft/draft.h draft/draftmanager.cpp draft/draftmanager.h # EWS integration ews/ewsattachment.cpp ews/ewsattachment.h ews/ewsattendee.cpp ews/ewsattendee.h ews/ewsclient_debug.cpp ews/ewsclient_debug.h ews/ewsid.cpp ews/ewsid.h ews/ewsitem.cpp ews/ewsitem.h ews/ewsitembase.cpp ews/ewsitembase.h ews/ewsitembase_p.h ews/ewsmailbox.cpp ews/ewsmailbox.h ews/ewsmailfactory.cpp ews/ewsmailfactory.h ews/ewsoccurrence.cpp ews/ewsoccurrence.h ews/ewspropertyfield.cpp ews/ewspropertyfield.h ews/ewsrecurrence.cpp ews/ewsrecurrence.h ews/ewsserverversion.cpp ews/ewsserverversion.h ews/ewstypes.cpp ews/ewstypes.h ews/ewsxml.cpp ews/ewsxml.h # Editor editor/composer.cpp editor/composer.h editor/composerviewbase.cpp editor/composerviewbase.h editor/composerdialog.cpp editor/composerdialog.h editor/cryptostateindicatorwidget.cpp editor/cryptostateindicatorwidget.h editor/encryptionstate.cpp editor/encryptionstate.h editor/kmcomposerglobalaction.cpp editor/kmcomposerglobalaction.h editor/nearexpirywarning.cpp editor/nearexpirywarning.h editor/mailtemplates.cpp editor/mailtemplates.h editor/recipient.cpp editor/recipient.h editor/recipientline.cpp editor/recipientline.h editor/recipientseditor.cpp editor/recipientseditor.h editor/util.h editor/util.cpp editor/richtextcomposerng.cpp editor/richtextcomposerng.h editor/richtextcomposersignatures.cpp editor/richtextcomposersignatures.h editor/nodehelper.cpp editor/nodehelper.h editor/signaturecontroller.cpp editor/signaturecontroller.h editor/spellcheckerconfigdialog.cpp editor/spellcheckerconfigdialog.h # Editor job editor/job/abstractencryptjob.h editor/job/autocryptheadersjob.h editor/job/contentjobbase.h editor/job/contentjobbase_p.h editor/job/dndfromarkjob.cpp editor/job/dndfromarkjob.h editor/job/encryptjob.h editor/job/inserttextfilejob.h editor/job/itipjob.h editor/job/jobbase.h editor/job/jobbase_p.h editor/job/maintextjob.h editor/job/multipartjob.h editor/job/protectedheadersjob.h editor/job/signencryptjob.h editor/job/signjob.h editor/job/singlepartjob.h editor/job/skeletonmessagejob.h editor/job/transparentjob.h editor/job/autocryptheadersjob.cpp editor/job/contentjobbase.cpp editor/job/encryptjob.cpp editor/job/inserttextfilejob.cpp editor/job/itipjob.cpp editor/job/jobbase.cpp editor/job/maintextjob.cpp editor/job/multipartjob.cpp editor/job/protectedheadersjob.cpp editor/job/saveasfilejob.cpp editor/job/saveasfilejob.h editor/job/signencryptjob.cpp editor/job/signjob.cpp editor/job/singlepartjob.cpp editor/job/skeletonmessagejob.cpp editor/job/transparentjob.cpp ## Editor Part editor/part/globalpart.h editor/part/infopart.h editor/part/itippart.h editor/part/messagepart.h editor/part/textpart.h editor/part/globalpart.cpp editor/part/infopart.cpp editor/part/itippart.cpp editor/part/messagepart.cpp editor/part/textpart.cpp ## Attachment editor/attachment/attachmentjob.cpp editor/attachment/attachmentjob.h editor/attachment/attachmentclipboardjob.cpp editor/attachment/attachmentclipboardjob.h editor/attachment/attachmentcompressjob.cpp editor/attachment/attachmentcompressjob.h editor/attachment/attachmentcontroller.cpp editor/attachment/attachmentcontroller.h editor/attachment/attachmentcontrollerbase.cpp editor/attachment/attachmentcontrollerbase.h editor/attachment/attachmentfromfolderjob.cpp editor/attachment/attachmentfromfolderjob.h editor/attachment/attachmentfrommimecontentjob.cpp editor/attachment/attachmentfrommimecontentjob.h editor/attachment/attachmentfromurlbasejob.cpp editor/attachment/attachmentfromurlbasejob.h editor/attachment/attachmentfromurljob.cpp editor/attachment/attachmentfromurljob.h editor/attachment/attachmentfromurlutils.cpp editor/attachment/attachmentfromurlutils.h editor/attachment/attachmentfrompublickeyjob.cpp editor/attachment/attachmentfrompublickeyjob.h editor/attachment/attachmentloadjob.cpp editor/attachment/attachmentloadjob.h editor/attachment/attachmentmodel.cpp editor/attachment/attachmentmodel.h editor/attachment/attachmentpart.cpp editor/attachment/attachmentpart.h editor/attachment/attachmentpropertiesdialog.cpp editor/attachment/attachmentpropertiesdialog.h editor/attachment/attachmentupdatejob.cpp editor/attachment/attachmentupdatejob.h editor/attachment/attachmentview.cpp editor/attachment/attachmentview.h editor/attachment/editorwatcher.cpp editor/attachment/editorwatcher.h ) ki18n_wrap_ui(gpgol-server-static editor/attachment/ui/attachmentpropertiesdialog.ui editor/attachment/ui/attachmentpropertiesdialog_readonly.ui ) ecm_qt_declare_logging_category(gpgol-server-static_SRCS HEADER websocket_debug.h IDENTIFIER WEBSOCKET_LOG CATEGORY_NAME org.gpgol.server.websocket DESCRIPTION "Websocket connection in the server" EXPORT GPGOL ) ecm_qt_declare_logging_category(gpgol-server-static_SRCS HEADER ewsresource_debug.h IDENTIFIER EWSRES_LOG CATEGORY_NAME org.gpgol.ews DESCRIPTION "Ews mail client" EXPORT GPGOL ) ecm_qt_declare_logging_category(gpgol-server-static_SRCS HEADER ewscli_debug.h IDENTIFIER EWSCLI_LOG CATEGORY_NAME org.gpgol.ews.client DESCRIPTION "ews client (gpgol-server)" EXPORT GPGOL ) ecm_qt_declare_logging_category(gpgol-server-static_SRCS HEADER editor_debug.h IDENTIFIER EDITOR_LOG CATEGORY_NAME org.gpgol.editor DESCRIPTION "mail composer" EXPORT GPGOL ) set(WARN_TOOMANY_RECIPIENTS_DEFAULT true) set(ALLOW_SEMICOLON_AS_ADDRESS_SEPARATOR_DEFAULT true) configure_file(editor/settings/messagecomposer.kcfg.in ${CMAKE_CURRENT_BINARY_DIR}/messagecomposer.kcfg) kconfig_add_kcfg_files(gpgol-server-static_SRCS editor/settings/messagecomposersettings.kcfgc ) install(FILES composerui.rc DESTINATION ${KDE_INSTALL_KXMLGUIDIR}/gpgol-server) target_sources(gpgol-server-static PUBLIC ${gpgol-server-static_SRCS}) target_link_libraries(gpgol-server-static PUBLIC Qt6::HttpServer Qt6::Widgets Qt6::PrintSupport KF6::CalendarCore KF6::ConfigCore KF6::ConfigGui KF6::Contacts KF6::Completion KF6::CoreAddons KF6::ColorScheme KF6::Codecs KF6::GuiAddons KF6::SonnetUi KF6::WidgetsAddons KF6::XmlGui KF6::KIOFileWidgets KF6::Archive KF6::TextAutoCorrectionCore KPim6::MimeTreeParserWidgets KPim6::Libkleo KPim6::Libkdepim KPim6::LdapWidgets KPim6::PimTextEdit KPim6::IdentityManagementCore KPim6::IdentityManagementWidgets ) add_executable(gpgol-server main.cpp) target_link_libraries(gpgol-server PRIVATE gpgol-server-static) diff --git a/server/composerwindowfactory.cpp b/server/composerwindowfactory.cpp new file mode 100644 index 0000000..30bfc19 --- /dev/null +++ b/server/composerwindowfactory.cpp @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2023 g10 code Gmbh +// SPDX-Contributor: Carl Schwan +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "composerwindowfactory.h" +#include "editor/composerdialog.h" + +ComposerWindowFactory::ComposerWindowFactory() = default; + +ComposerWindowFactory &ComposerWindowFactory::self() +{ + static ComposerWindowFactory instance; + return instance; +} + +ComposerDialog *ComposerWindowFactory::create(const QString &fromAddress, const QString &name, const QByteArray &bearerToken) +{ + if (inactiveWindow) { + auto window = inactiveWindow; + inactiveWindow = nullptr; + window->reset(fromAddress, name, bearerToken); + return window; + } + + return new ComposerDialog(fromAddress, name, bearerToken); +} + +void ComposerWindowFactory::clear(ComposerDialog *composerWindow) +{ + if (inactiveWindow) { + composerWindow->deleteLater(); + return; + } + + inactiveWindow = composerWindow; + inactiveWindow->hide(); +} diff --git a/server/composerwindowfactory.h b/server/composerwindowfactory.h new file mode 100644 index 0000000..9a8b7ab --- /dev/null +++ b/server/composerwindowfactory.h @@ -0,0 +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 ComposerDialog; + +/// 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. + ComposerDialog *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(ComposerDialog *composerWindow); + +private: + ComposerWindowFactory(); + + ComposerDialog * inactiveWindow = nullptr; +}; diff --git a/server/controllers/emailcontroller.cpp b/server/controllers/emailcontroller.cpp index 25dea61..68fd94c 100644 --- a/server/controllers/emailcontroller.cpp +++ b/server/controllers/emailcontroller.cpp @@ -1,198 +1,193 @@ // 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 "../editor/composerdialog.h" #include "../draft/draftmanager.h" +#include "composerwindowfactory.h" using namespace Qt::Literals::StringLiterals; namespace { QByteArray findHeader(QList> headers, const QByteArray &key) { const auto it = std::find_if(std::cbegin(headers), std::cend(headers), [&key](auto header) { return header.first == key; }); if (it == std::cend(headers)) { return {}; } return it->second; } } QHttpServerResponse EmailController::viewEmailAction(const QHttpServerRequest &request) { const auto email = QString::fromUtf8(findHeader(request.headers(), "X-EMAIL")); const auto displayName = QString::fromUtf8(findHeader(request.headers(), "X-NAME")); const auto bearerToken = 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 }); dialog->setAttribute(Qt::WA_DeleteOnClose); auto toolBar = dialog->toolBar(); toolBar->show(); // spacer 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 = new ComposerDialog(email, displayName, bearerToken); - dialog->setAttribute(Qt::WA_DeleteOnClose); + 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 = new ComposerDialog(email, displayName, bearerToken); - dialog->setAttribute(Qt::WA_DeleteOnClose); + 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 } }); } 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() }, }); } QHttpServerResponse EmailController::newEmailAction(const QHttpServerRequest &request) { const auto email = QString::fromUtf8(findHeader(request.headers(), "X-EMAIL")); const auto displayName = QString::fromUtf8(findHeader(request.headers(), "X-NAME")); const auto bearerToken = findHeader(request.headers(), "X-TOKEN"); - auto dialog = new ComposerDialog(email, displayName, bearerToken); - dialog->setAttribute(Qt::WA_DeleteOnClose); + auto dialog = ComposerWindowFactory::self().create(email, displayName, bearerToken); dialog->show(); return QHttpServerResponse(QJsonObject { { "status"_L1, "200"_L1 } }); } QHttpServerResponse EmailController::replyEmailAction(const QHttpServerRequest &request) { const auto email = QString::fromUtf8(findHeader(request.headers(), "X-EMAIL")); const auto displayName = QString::fromUtf8(findHeader(request.headers(), "X-NAME")); const auto bearerToken = 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 ComposerDialog(email, displayName, bearerToken); - dialog->setAttribute(Qt::WA_DeleteOnClose); + auto dialog = ComposerWindowFactory::self().create(email, displayName, bearerToken); dialog->reply(message); dialog->show(); return QHttpServerResponse(QJsonObject { { "status"_L1, "200"_L1 } }); } QHttpServerResponse EmailController::forwardEmailAction(const QHttpServerRequest &request) { const auto email = QString::fromUtf8(findHeader(request.headers(), "X-EMAIL")); const auto displayName = QString::fromUtf8(findHeader(request.headers(), "X-NAME")); const auto bearerToken = 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 ComposerDialog(email, displayName, bearerToken); - dialog->setAttribute(Qt::WA_DeleteOnClose); + auto dialog = ComposerWindowFactory::self().create(email, displayName, bearerToken); dialog->forward(message); dialog->show(); return QHttpServerResponse(QJsonObject { { "status"_L1, "200"_L1 } }); } QHttpServerResponse EmailController::draftAction(QString draftId, const QHttpServerRequest &request) { const auto email = QString::fromUtf8(findHeader(request.headers(), "X-EMAIL")); const auto displayName = QString::fromUtf8(findHeader(request.headers(), "X-NAME")); const auto bearerToken = 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); } if (request.method() == QHttpServerRequest::Method::Post) { // POST - auto dialog = new ComposerDialog(email, displayName, bearerToken); - dialog->setAttribute(Qt::WA_DeleteOnClose); + 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, "200"_L1 } }); } diff --git a/server/editor/attachment/attachmentcontrollerbase.cpp b/server/editor/attachment/attachmentcontrollerbase.cpp index fb7b714..31aeaf3 100644 --- a/server/editor/attachment/attachmentcontrollerbase.cpp +++ b/server/editor/attachment/attachmentcontrollerbase.cpp @@ -1,951 +1,959 @@ /* * 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/attachmentfrompublickeyjob.h" #include "../attachment/attachmentfromurlbasejob.h" #include "../attachment/attachmentcompressjob.h" #include "../attachment/attachmentclipboardjob.h" #include "../attachment/attachmentfromurlutils.h" #include "../attachment/attachmentpropertiesdialog.h" #include "../composer.h" #include "../part/globalpart.h" #include "messagecomposersettings.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 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 editSelectedAttachment(); // slot void editSelectedAttachmentWith(); // slot void removeSelectedAttachments(); // slot void saveSelectedAttachmentAs(); // slot void selectedAttachmentProperties(); // slot void editDone(MessageComposer::EditorWatcher *watcher); // slot void attachPublicKeyJobResult(KJob *job); // slot void slotAttachmentContentCreated(KJob *job); // slot void addAttachmentPart(AttachmentPart::Ptr part); void attachClipBoardElement(KJob *job); void selectedAllAttachment(); void createOpenWithMenu(QMenu *topMenu, const AttachmentPart::Ptr &part); void reloadAttachment(); void updateJobResult(KJob *); AttachmentPart::List selectedParts; AttachmentControllerBase *const q; MessageComposer::AttachmentModel *model = nullptr; QWidget *wParent = nullptr; QHash editorPart; QHash editorTempFile; KActionCollection *mActionCollection = nullptr; QAction *attachPublicKeyAction = nullptr; QAction *attachMyPublicKeyAction = nullptr; QAction *openContextAction = nullptr; QAction *viewContextAction = nullptr; QAction *editContextAction = nullptr; QAction *editWithContextAction = nullptr; QAction *removeAction = nullptr; QAction *removeContextAction = nullptr; QAction *saveAsAction = nullptr; QAction *saveAsContextAction = nullptr; QAction *propertiesAction = nullptr; QAction *propertiesContextAction = nullptr; QAction *addAttachmentFileAction = nullptr; QAction *addAttachmentDirectoryAction = nullptr; QAction *addContextAction = nullptr; QAction *selectAllAction = nullptr; KActionMenu *attachmentMenu = nullptr; QAction *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(); const bool enableEditAction = (selectedCount == 1) && (!selectedParts.first()->isMessageOrMessageCollection()); d->openContextAction->setEnabled(selectedCount > 0); d->viewContextAction->setEnabled(selectedCount > 0); d->editContextAction->setEnabled(enableEditAction); d->editWithContextAction->setEnabled(enableEditAction); d->removeAction->setEnabled(selectedCount > 0); d->removeContextAction->setEnabled(selectedCount > 0); d->saveAsAction->setEnabled(selectedCount == 1); d->saveAsContextAction->setEnabled(selectedCount == 1); d->propertiesAction->setEnabled(selectedCount == 1); d->propertiesContextAction->setEnabled(selectedCount == 1); } void AttachmentControllerBase::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::editSelectedAttachment() { Q_ASSERT(selectedParts.count() == 1); q->editAttachment(selectedParts.constFirst(), MessageComposer::EditorWatcher::NoOpenWithDialog); } void AttachmentControllerBase::AttachmentControllerBasePrivate::editSelectedAttachmentWith() { Q_ASSERT(selectedParts.count() == 1); q->editAttachment(selectedParts.constFirst(), MessageComposer::EditorWatcher::OpenWithDialog); } 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) { model->removeAttachment(part); } } 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::AttachmentControllerBasePrivate::editDone(MessageComposer::EditorWatcher *watcher) { AttachmentPart::Ptr part = editorPart.take(watcher); Q_ASSERT(part); QTemporaryFile *tempFile = editorTempFile.take(watcher); Q_ASSERT(tempFile); if (watcher->fileChanged()) { qCDebug(EDITOR_LOG) << "File has changed."; const QString name = watcher->url().path(); QFile file(name); if (file.open(QIODevice::ReadOnly)) { const QByteArray data = file.readAll(); part->setData(data); model->updateAttachment(part); } } delete tempFile; // The watcher deletes itself. } void AttachmentControllerBase::AttachmentControllerBasePrivate::createOpenWithMenu(QMenu *topMenu, const AttachmentPart::Ptr &part) { const QString contentTypeStr = QString::fromLatin1(part->mimeType()); const KService::List offers = KFileItemActions::associatedApplications(QStringList() << contentTypeStr); if (!offers.isEmpty()) { QMenu *menu = topMenu; auto actionGroup = new QActionGroup(menu); connect(actionGroup, &QActionGroup::triggered, q, &AttachmentControllerBase::slotOpenWithAction); if (offers.count() > 1) { // submenu 'open with' menu = new QMenu(i18nc("@title:menu", "&Open With"), topMenu); menu->menuAction()->setObjectName(QLatin1StringView("openWith_submenu")); // for the unittest topMenu->addMenu(menu); } // qCDebug(EDITOR_LOG) << offers.count() << "offers" << topMenu << menu; KService::List::ConstIterator it = offers.constBegin(); KService::List::ConstIterator end = offers.constEnd(); for (; it != end; ++it) { /* TODO Carl QAction *act = MessageViewer::Util::createAppAction(*it, // no submenu -> prefix single offer menu == topMenu, actionGroup, menu); menu->addAction(act); */ } QString openWithActionName; if (menu != topMenu) { // submenu menu->addSeparator(); openWithActionName = i18nc("@action:inmenu Open With", "&Other..."); } else { openWithActionName = i18nc("@title:menu", "&Open With..."); } auto openWithAct = new QAction(menu); openWithAct->setText(openWithActionName); QObject::connect(openWithAct, &QAction::triggered, q, &AttachmentControllerBase::slotOpenWithDialog); menu->addAction(openWithAct); } else { // no app offers -> Open With... auto act = new QAction(topMenu); act->setText(i18nc("@title:menu", "&Open With...")); QObject::connect(act, &QAction::triggered, q, &AttachmentControllerBase::slotOpenWithDialog); topMenu->addAction(act); } } void AttachmentControllerBase::exportPublicKey(const 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->editContextAction = new QAction(i18nc("to edit", "Edit"), this); connect(d->editContextAction, &QAction::triggered, this, [this]() { d->editSelectedAttachment(); }); d->editWithContextAction = new QAction(i18n("Edit With..."), this); connect(d->editWithContextAction, &QAction::triggered, this, [this]() { d->editSelectedAttachmentWith(); }); d->saveAsAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("&Save Attachment As..."), this); d->saveAsContextAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("Save As..."), this); connect(d->saveAsAction, &QAction::triggered, this, [this]() { d->saveSelectedAttachmentAs(); }); connect(d->saveAsContextAction, &QAction::triggered, this, [this]() { d->saveSelectedAttachmentAs(); }); d->propertiesAction = new QAction(i18n("Attachment Pr&operties..."), this); d->propertiesContextAction = new QAction(i18n("Properties"), this); connect(d->propertiesAction, &QAction::triggered, this, [this]() { d->selectedAttachmentProperties(); }); connect(d->propertiesContextAction, &QAction::triggered, this, [this]() { d->selectedAttachmentProperties(); }); d->selectAllAction = new QAction(i18n("Select All"), this); connect(d->selectAllAction, &QAction::triggered, this, &AttachmentControllerBase::selectedAllAttachment); d->reloadAttachmentAction = new QAction(QIcon::fromTheme(QStringLiteral("view-refresh")), i18n("Reload"), this); connect(d->reloadAttachmentAction, &QAction::triggered, this, [this]() { d->reloadAttachment(); }); // Insert the actions into the composer window's menu. KActionCollection *collection = d->mActionCollection; collection->addAction(QStringLiteral("attach_public_key"), d->attachPublicKeyAction); collection->addAction(QStringLiteral("attach_my_public_key"), d->attachMyPublicKeyAction); collection->addAction(QStringLiteral("attach"), d->addAttachmentFileAction); collection->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; const bool enableEditAction = (numberOfParts == 1) && (!d->selectedParts.first()->isMessageOrMessageCollection()); if (numberOfParts > 0) { if (numberOfParts == 1) { const QString mimetype = QString::fromLatin1(d->selectedParts.first()->mimeType()); QMimeDatabase mimeDb; auto mime = mimeDb.mimeTypeForName(mimetype); QStringList parentMimeType; if (mime.isValid()) { parentMimeType = mime.allAncestors(); } if ((mimetype == QLatin1String("text/plain")) || (mimetype == QLatin1String("image/png")) || (mimetype == QLatin1String("image/jpeg")) || parentMimeType.contains(QLatin1String("text/plain")) || parentMimeType.contains(QLatin1String("image/png")) || parentMimeType.contains(QLatin1String("image/jpeg"))) { menu.addAction(d->viewContextAction); } d->createOpenWithMenu(&menu, d->selectedParts.constFirst()); } menu.addAction(d->openContextAction); } if (enableEditAction) { menu.addAction(d->editWithContextAction); menu.addAction(d->editContextAction); } menu.addSeparator(); if (numberOfParts == 1) { if (!d->selectedParts.first()->url().isEmpty()) { menu.addAction(d->reloadAttachmentAction); } menu.addAction(d->saveAsContextAction); menu.addSeparator(); menu.addAction(d->propertiesContextAction); menu.addSeparator(); } if (numberOfParts > 0) { menu.addAction(d->removeContextAction); menu.addSeparator(); } const int nbAttachment = d->model->rowCount(); if (nbAttachment != numberOfParts) { menu.addAction(d->selectAllAction); menu.addSeparator(); } if (numberOfParts == 0) { menu.addAction(d->addContextAction); } menu.exec(QCursor::pos()); } void AttachmentControllerBase::slotOpenWithDialog() { openWith(); } void AttachmentControllerBase::slotOpenWithAction(QAction *act) { auto app = act->data().value(); Q_ASSERT(d->selectedParts.count() == 1); openWith(app); } void AttachmentControllerBase::openWith(const KService::Ptr &offer) { QTemporaryFile *tempFile = dumpAttachmentToTempFile(d->selectedParts.constFirst()); 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; } QUrl url = QUrl::fromLocalFile(tempFile->fileName()); tempFile->setPermissions(QFile::ReadUser); // If offer is null, this will show the "open with" dialog auto job = new KIO::ApplicationLauncherJob(offer); job->setUrls({url}); job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, d->wParent)); job->start(); connect(job, &KJob::result, this, [tempFile, job]() { if (job->error()) { delete tempFile; } }); // Delete the file only when the composer is closed // (and this object is destroyed). tempFile->setParent(this); // Manages lifetime. } void AttachmentControllerBase::openAttachment(const AttachmentPart::Ptr &part) { QTemporaryFile *tempFile = dumpAttachmentToTempFile(part); if (!tempFile) { KMessageBox::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); auto job = new KIO::OpenUrlJob(QUrl::fromLocalFile(tempFile->fileName()), QString::fromLatin1(part->mimeType())); job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, d->wParent)); job->setDeleteTemporaryFile(true); connect(job, &KIO::OpenUrlJob::result, this, [this, tempFile](KJob *job) { if (job->error() == KIO::ERR_USER_CANCELED) { KMessageBox::error(d->wParent, i18n("KMail was unable to open the attachment."), job->errorString()); delete tempFile; } else { // The file was opened. Delete it only when the composer is closed // (and this object is destroyed). tempFile->setParent(this); // Manages lifetime. } }); job->start(); } 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(), QByteArray()); } } else { // TODO: show warning to the user qCWarning(EDITOR_LOG) << "Error creating KMime::Content for attachment:" << job->errorText(); } } void AttachmentControllerBase::editAttachment(AttachmentPart::Ptr part, MessageComposer::EditorWatcher::OpenWithOption openWithOption) { 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 edit attachment")); return; } auto watcher = new MessageComposer::EditorWatcher(QUrl::fromLocalFile(tempFile->fileName()), QString::fromLatin1(part->mimeType()), openWithOption, this, d->wParent); connect(watcher, &MessageComposer::EditorWatcher::editDone, this, [this](MessageComposer::EditorWatcher *watcher) { d->editDone(watcher); }); switch (watcher->start()) { case MessageComposer::EditorWatcher::NoError: // The attachment is being edited. // We will clean things up in editDone(). d->editorPart[watcher] = part; d->editorTempFile[watcher] = tempFile; // Delete the temp file if the composer is closed (and this object is destroyed). tempFile->setParent(this); // Manages lifetime. break; case MessageComposer::EditorWatcher::CannotStart: qCWarning(EDITOR_LOG) << "Could not start EditorWatcher."; [[fallthrough]]; case MessageComposer::EditorWatcher::Unknown: case MessageComposer::EditorWatcher::Canceled: case MessageComposer::EditorWatcher::NoServiceFound: delete watcher; delete tempFile; break; } } void AttachmentControllerBase::editAttachmentWith(const AttachmentPart::Ptr &part) { editAttachment(part, MessageComposer::EditorWatcher::OpenWithDialog); } void AttachmentControllerBase::saveAttachmentAs(const AttachmentPart::Ptr &part) { QString pname = part->name(); if (pname.isEmpty()) { pname = i18n("unnamed"); } const QUrl url = QFileDialog::getSaveFileUrl(d->wParent, i18n("Save Attachment As"), QUrl::fromLocalFile(pname)); if (url.isEmpty()) { qCDebug(EDITOR_LOG) << "Save Attachment As dialog canceled."; return; } byteArrayToRemoteFile(part->data(), url); } void AttachmentControllerBase::byteArrayToRemoteFile(const QByteArray &aData, const QUrl &aURL, bool overwrite) { KIO::StoredTransferJob *job = KIO::storedPut(aData, aURL, -1, overwrite ? KIO::Overwrite : KIO::DefaultFlags); connect(job, &KIO::StoredTransferJob::result, this, &AttachmentControllerBase::slotPutResult); } void AttachmentControllerBase::slotPutResult(KJob *job) { auto _job = qobject_cast(job); if (job->error()) { if (job->error() == KIO::ERR_FILE_ALREADY_EXIST) { if (KMessageBox::warningContinueCancel(nullptr, i18n("File %1 exists.\nDo you want to replace it?", _job->url().toLocalFile()), i18nc("@title:window", "Save to File"), KGuiItem(i18n("&Replace"))) == KMessageBox::Continue) { byteArrayToRemoteFile(_job->data(), _job->url(), true); } } else { KJobUiDelegate *ui = static_cast(job)->uiDelegate(); ui->showErrorMessage(); } } } void AttachmentControllerBase::attachmentProperties(const AttachmentPart::Ptr &part) { QPointer dialog = new AttachmentPropertiesDialog(part, false, d->wParent); dialog->setEncryptEnabled(d->encryptEnabled); dialog->setSignEnabled(d->signEnabled); if (dialog->exec() && dialog) { d->model->updateAttachment(part); } delete dialog; } void AttachmentControllerBase::attachDirectory(const QUrl &url) { const int rc = KMessageBox::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); } } static QString fixEncoding(const QString &encoding) { QString returnEncoding = encoding; // According to https://www.iana.org/assignments/character-sets, uppercase is // preferred in MIME headers const QString returnEncodingToUpper = returnEncoding.toUpper(); if (returnEncodingToUpper.contains(QLatin1String("ISO "))) { returnEncoding = returnEncodingToUpper; returnEncoding.replace(QLatin1String("ISO "), QStringLiteral("ISO-")); } return returnEncoding; } void AttachmentControllerBase::showAddAttachmentFileDialog() { const KEncodingFileDialog::Result result = KEncodingFileDialog::getOpenUrlsAndEncoding(QString(), QUrl(), QString(), d->wParent, i18nc("@title:window", "Attach File")); if (!result.URLs.isEmpty()) { const QString encoding = fixEncoding(result.encoding); const int numberOfFiles(result.URLs.count()); for (int i = 0; i < numberOfFiles; ++i) { const QUrl url = result.URLs.at(i); QUrl urlWithEncoding = url; QUrlQuery query; query.addQueryItem(QStringLiteral("charset"), encoding); urlWithEncoding.setQuery(query); QMimeDatabase mimeDb; const auto mimeType = mimeDb.mimeTypeForUrl(urlWithEncoding); 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(urlWithEncoding); } } else { addAttachment(urlWithEncoding); } } } } void AttachmentControllerBase::addAttachment(const AttachmentPart::Ptr &part) { part->setEncrypted(d->model->isEncryptSelected()); part->setSigned(d->model->isSignSelected()); d->model->addAttachment(part); Q_EMIT fileAttached(); } void AttachmentControllerBase::addAttachmentUrlSync(const QUrl &url) { MessageCore::AttachmentFromUrlBaseJob *ajob = MessageCore::AttachmentFromUrlUtils::createAttachmentJob(url, this); if (ajob->exec()) { AttachmentPart::Ptr part = ajob->attachmentPart(); addAttachment(part); } else { if (ajob->error()) { KMessageBox::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) { + d->model->removeAttachment(attachmentPart); + } +} + #include "moc_attachmentcontrollerbase.cpp" diff --git a/server/editor/attachment/attachmentcontrollerbase.h b/server/editor/attachment/attachmentcontrollerbase.h index 79bb97b..d1e82e0 100644 --- a/server/editor/attachment/attachmentcontrollerbase.h +++ b/server/editor/attachment/attachmentcontrollerbase.h @@ -1,94 +1,95 @@ /* * 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 */ #pragma once #include "editorwatcher.h" #include #include #include #include #include "attachmentpart.h" class KActionCollection; class QAction; class KJob; namespace MessageComposer { class AttachmentModel; /** * @brief The AttachmentControllerBase class */ class AttachmentControllerBase : public QObject { Q_OBJECT public: AttachmentControllerBase(MessageComposer::AttachmentModel *model, QWidget *wParent, KActionCollection *actionCollection); ~AttachmentControllerBase() override; void createActions(); // TODO dnd stuff... void setSelectedParts(const MessageCore::AttachmentPart::List &selectedParts); public Q_SLOTS: /// model sets these void setEncryptEnabled(bool enabled); void setSignEnabled(bool enabled); /// compression is async... void compressAttachment(const MessageCore::AttachmentPart::Ptr &part, bool compress); void showContextMenu(); void openAttachment(const MessageCore::AttachmentPart::Ptr &part); void viewAttachment(const MessageCore::AttachmentPart::Ptr &part); void editAttachment(MessageCore::AttachmentPart::Ptr part, MessageComposer::EditorWatcher::OpenWithOption option = MessageComposer::EditorWatcher::NoOpenWithDialog); void editAttachmentWith(const MessageCore::AttachmentPart::Ptr &part); void saveAttachmentAs(const MessageCore::AttachmentPart::Ptr &part); void attachmentProperties(const MessageCore::AttachmentPart::Ptr &part); void showAddAttachmentFileDialog(); void showAddAttachmentCompressedDirectoryDialog(); /// sets sign, encrypt, shows properties dialog if so configured void addAttachment(const MessageCore::AttachmentPart::Ptr &part); void addAttachment(const QUrl &url); void addAttachmentUrlSync(const QUrl &url); void addAttachments(const QList &urls); void showAttachPublicKeyDialog(); void showAttachClipBoard(); virtual void attachMyPublicKey(); + void clear(); Q_SIGNALS: void actionsCreated(); void refreshSelection(); void showAttachment(KMime::Content *content, const QByteArray &charset); void selectedAllAttachment(); void fileAttached(); protected: void exportPublicKey(const GpgME::Key &key); void enableAttachPublicKey(bool enable); void enableAttachMyPublicKey(bool enable); void byteArrayToRemoteFile(const QByteArray &aData, const QUrl &aURL, bool overwrite = false); void openWith(const KService::Ptr &offer = KService::Ptr()); private: void attachDirectory(const QUrl &url); void slotPutResult(KJob *job); void slotOpenWithDialog(); void slotOpenWithAction(QAction *act); private: class AttachmentControllerBasePrivate; std::unique_ptr const d; }; } // diff --git a/server/editor/composerdialog.cpp b/server/editor/composerdialog.cpp index f203bd2..26ff0b9 100644 --- a/server/editor/composerdialog.cpp +++ b/server/editor/composerdialog.cpp @@ -1,1374 +1,1570 @@ // SPDX-FileCopyrightText: 2023 g10 code Gmbh // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "composerdialog.h" +// Qt includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include +// KDE 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 +// Gpgme includes #include #include -#include +// App includes #include "../identity/identitymanager.h" #include "../identity/identitycombo.h" #include "../identity/identitydialog.h" #include "recipientseditor.h" #include "nearexpirywarning.h" #include "cryptostateindicatorwidget.h" #include "composerviewbase.h" #include "richtextcomposerng.h" #include "signaturecontroller.h" #include "job/dndfromarkjob.h" #include "job/saveasfilejob.h" #include "job/inserttextfilejob.h" #include "attachment/attachmentcontroller.h" #include "attachment/attachmentview.h" #include "attachment/attachmentmodel.h" #include "kmcomposerglobalaction.h" #include "mailtemplates.h" #include "messagecomposersettings.h" #include "spellcheckerconfigdialog.h" #include "websocketclient.h" #include "draft/draftmanager.h" using namespace Qt::Literals::StringLiterals; namespace { 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); }); } } ComposerDialog::ComposerDialog(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)) , mCryptoStateIndicatorWidget(new CryptoStateIndicatorWidget(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()) { bool isNew = false; mIdentity = IdentityManager::self().fromEmail(from, isNew); mEdtFrom = new IdentityCombo(mHeadersArea); 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); 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; // 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->setCurrentIdentity(mFrom); mEdtFrom->setObjectName(QStringLiteral("fromLine")); connect(mEdtFrom, &QComboBox::currentIndexChanged, this, &ComposerDialog::slotIdentityChanged); fromWrapperLayout->addWidget(mEdtFrom); mComposerBase->setIdentityCombo(mEdtFrom); mComposerBase->setIdentityManager(&IdentityManager::self()); mButtonFrom->setText(i18nc("@action:button", "Configure")); mButtonFrom->setIcon(QIcon::fromTheme(u"configure-symbolic"_s)); connect(mButtonFrom, &QPushButton::clicked, this, &ComposerDialog::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); 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); // Crypto indicator vLayout->addWidget(mCryptoStateIndicatorWidget); mCryptoStateIndicatorWidget->setObjectName(QStringLiteral("CryptoIndicator")); // 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())); - deleteLater(); + 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, &ComposerDialog::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, &ComposerDialog::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); updateSignatureAndEncryptionStateIndicators(); 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) { mEncryptionState.setAcceptedSolution(false); iconname = QStringLiteral("emblem-error"); const auto showCryptoIndicator = true; const auto hasOverride = mEncryptionState.hasOverride(); const auto encrypt = mEncryptionState.encrypt(); const bool showAllIcons = showCryptoIndicator && hasOverride && 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(); } +void ComposerDialog::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(); +} + void ComposerDialog::setupActions() { // Save as draft auto action = new QAction(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save as &Draft"), this); actionCollection()->addAction(QStringLiteral("save_in_drafts"), action); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL | Qt::Key_S)); connect(action, &QAction::triggered, this, &ComposerDialog::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, &ComposerDialog::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, &ComposerDialog::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, &ComposerDialog::slotAutoSpellCheckingToggled); connect(mComposerBase->editor(), &TextCustomEditor::RichTextEditor::checkSpellingChanged, this, &ComposerDialog::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, &ComposerDialog::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, &ComposerDialog::slotInsertRecentFile); connect(mRecentAction, &KRecentFilesAction::recentListCleared, this, &ComposerDialog::slotRecentListFileClear); const QStringList urls = MessageComposer::MessageComposerSettings::self()->recentUrls(); for (const QString &url : urls) { mRecentAction->addUrl(QUrl(url)); } // print KStandardAction::print(this, &ComposerDialog::slotPrint, actionCollection()); KStandardAction::printPreview(this, &ComposerDialog::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, &ComposerDialog::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, &ComposerDialog::slotToggleMarkup); mWordWrapAction = new KToggleAction(i18n("&Wordwrap"), this); actionCollection()->addAction(QStringLiteral("wordwrap"), mWordWrapAction); mWordWrapAction->setChecked(MessageComposer::MessageComposerSettings::self()->wordWrap()); connect(mWordWrapAction, &KToggleAction::toggled, this, &ComposerDialog::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(&mEncryptionState, &EncryptionState::possibleEncryptChanged, mEncryptAction, &KToggleAction::setEnabled); connect(mEncryptAction, &KToggleAction::triggered, &mEncryptionState, &EncryptionState::toggleOverride); // 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(&mEncryptionState, &EncryptionState::possibleEncryptChanged, mEncryptAction, &KToggleAction::setEnabled); connect(&mEncryptionState, &EncryptionState::encryptChanged, mEncryptAction, &KToggleAction::setChecked); connect(mSignAction, &KToggleAction::triggered, this, &ComposerDialog::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, &ComposerDialog::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, &ComposerDialog::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, &ComposerDialog::htmlToolBarVisibilityChanged); connect(&mEncryptionState, &EncryptionState::encryptChanged, this, &ComposerDialog::slotEncryptionButtonIconUpdate); connect(&mEncryptionState, &EncryptionState::encryptChanged, this, &ComposerDialog::updateSignatureAndEncryptionStateIndicators); connect(&mEncryptionState, &EncryptionState::overrideChanged, this, &ComposerDialog::slotEncryptionButtonIconUpdate); connect(&mEncryptionState, &EncryptionState::overrideChanged, this, &ComposerDialog::runKeyResolver); connect(&mEncryptionState, &EncryptionState::acceptedSolutionChanged, this, &ComposerDialog::slotEncryptionButtonIconUpdate); } void ComposerDialog::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, &ComposerDialog::slotCursorPositionChanged); slotCursorPositionChanged(); } void ComposerDialog::reply(const KMime::Message::Ptr &originalMessage) { MailTemplates::reply(originalMessage, [this](const KMime::Message::Ptr &message) { setMessage(message); }); } void ComposerDialog::forward(const KMime::Message::Ptr &originalMessage) { MailTemplates::forward(originalMessage, [this](const KMime::Message::Ptr &message) { setMessage(message); }); } void ComposerDialog::setMessage(const KMime::Message::Ptr &msg) { mComposerBase->setMessage(msg, true); } void ComposerDialog::setModified(bool isModified) { mIsModified = isModified; } bool ComposerDialog::isModified() const { return mIsModified; } void ComposerDialog::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); if (!setByUser) { updateSignatureAndEncryptionStateIndicators(); } // mark the attachments for (no) signing //if (canSignEncryptAttachments()) { // mComposerBase->attachmentModel()->setSignSelected(sign); //} } std::unique_ptr ComposerDialog::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); return keyResolverCore; } void ComposerDialog::slotEncryptionButtonIconUpdate() { const auto state = mEncryptionState.encrypt(); const auto setByUser = mEncryptionState.override(); const auto acceptedSolution = mEncryptionState.acceptedSolution(); 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 (acceptedSolution) { auto overlay = QIcon::fromTheme(QStringLiteral("emblem-added")); if (state) { overlay = QIcon::fromTheme(QStringLiteral("emblem-checked")); } icon = KIconUtils::addOverlay(icon, overlay, Qt::BottomRightCorner); } else { if (state && setByUser) { auto overlay = QIcon::fromTheme(QStringLiteral("emblem-warning")); icon = KIconUtils::addOverlay(icon, overlay, Qt::BottomRightCorner); } } mEncryptAction->setIcon(icon); mEncryptAction->setToolTip(tooltip); } void ComposerDialog::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) { mEncryptionState.setAcceptedSolution(false); return; } } mEncryptionState.setAcceptedSolution(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); } } annotateRecipientEditorLineWithCrpytoInfo(line); if (!key.isNull()) { mExpiryChecker->checkKey(key, Kleo::ExpiryChecker::EncryptionKey); } } } void ComposerDialog::annotateRecipientEditorLineWithCrpytoInfo(RecipientLineNG *line) { auto recipient = line->data().dynamicCast(); const auto key = recipient->key(); const auto showCryptoIndicator = true; const auto hasOverride = mEncryptionState.hasOverride(); const auto encrypt = mEncryptionState.encrypt(); const bool showPositiveIcons = showCryptoIndicator && encrypt; const bool showAllIcons = showCryptoIndicator && hasOverride && 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 = NoKey; } 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."); } 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(); } if (keyState == NoKey) { mEncryptionState.setAcceptedSolution(false); if (showAllIcons) { line->setIcon(QIcon::fromTheme(QStringLiteral("emblem-error")), tooltip); } else { line->setIcon(QIcon()); } } else if (trustLevel == Kleo::Level0 && encrypt) { 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 ComposerDialog::slotSignToggled(bool on) { setSigning(on, true); updateSignatureAndEncryptionStateIndicators(); } void ComposerDialog::updateSignatureAndEncryptionStateIndicators() { mCryptoStateIndicatorWidget->updateSignatureAndEncrypionStateIndicators(sign(), mEncryptionState.encrypt()); } bool ComposerDialog::sign() const { return mSignAction->isChecked(); } void ComposerDialog::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 = mEncryptionState.encrypt(); mComposerBase->setCryptoOptions( sign(), encrypt, cryptoMessageFormat()); mComposerBase->send(); } void ComposerDialog::changeCryptoAction() { if (!QGpgME::openpgp() && !QGpgME::smime()) { // no crypto whatsoever mEncryptAction->setEnabled(false); mEncryptionState.setPossibleEncrypt(false); mSignAction->setEnabled(false); setSigning(false); } else { // TODO: carl const bool canOpenPGPSign = QGpgME::openpgp();// && !ident.pgpSigningKey().isEmpty(); const bool canSMIMESign = QGpgME::smime(); // && !ident.smimeSigningKey().isEmpty(); setSigning((canOpenPGPSign || canSMIMESign)); // && ident.pgpAutoSign()); } } void ComposerDialog::slotToggleMarkup() { htmlToolBarVisibilityChanged(mMarkupAction->isChecked()); } void ComposerDialog::htmlToolBarVisibilityChanged(bool visible) { if (visible) { enableHtml(); } else { disableHtml(LetUserConfirm); } } void ComposerDialog::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 ComposerDialog::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 ComposerDialog::expiryChecker() { if (!mExpiryChecker) { mExpiryChecker.reset(new Kleo::ExpiryChecker{Kleo::ExpiryCheckerSettings{encryptOwnKeyNearExpiryWarningThresholdInDays(), encryptKeyNearExpiryWarningThresholdInDays(), encryptRootCertNearExpiryWarningThresholdInDays(), encryptChainCertNearExpiryWarningThresholdInDays()}}); } return mExpiryChecker; } Kleo::CryptoMessageFormat ComposerDialog::cryptoMessageFormat() const { return Kleo::AutoFormat; } void ComposerDialog::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 ComposerDialog::slotIdentityChanged() { 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 = mEncryptionState.encrypt(); + } + + 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); + + mEncryptionState.setPossibleEncrypt(true); + changeCryptoAction(); + mEncryptionState.unsetOverride(); + mEncryptionState.setPossibleEncrypt(mEncryptionState.possibleEncrypt() && bNewIdentityHasEncryptionKey); + mEncryptionState.setAutoEncrypt(mIdentity.pgpAutoEncrypt()); + + // make sure the From and BCC fields are shown if necessary + if (mIdentity.pgpAutoEncrypt() && mKeyCache->initialized()) { + runKeyResolver(); + } Q_EMIT identityChanged(); } +void ComposerDialog::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 ComposerDialog::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 ComposerDialog::identity() const { return mIdentity; } void ComposerDialog::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 ComposerDialog::addAttachment(const QString &name, KMime::Headers::contentEncoding cte, const QString &charset, const QByteArray &data, const QByteArray &mimeType) { Q_UNUSED(cte) mComposerBase->addAttachment(name, name, charset, data, mimeType); } void ComposerDialog::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 ComposerDialog::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; } else { auto job = new DndFromArkJob(this); job->setComposerDialog(this); if (job->extract(source)) { 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 ComposerDialog::slotSaveDraft() { mComposerBase->autoSaveMessage(); } void ComposerDialog::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 ComposerDialog::insertFile() { const auto fileName = QFileDialog::getOpenFileName(this, i18nc("@title:window", "Insert File")); return QUrl::fromUserInput(fileName); } void ComposerDialog::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 ComposerDialog::slotRecentListFileClear() { MessageComposer::MessageComposerSettings::self()->setRecentUrls({}); MessageComposer::MessageComposerSettings::self()->save(); } void ComposerDialog::slotInsertRecentFile(const QUrl &u) { if (u.fileName().isEmpty()) { return; } auto job = new MessageComposer::InsertTextFileJob(mComposerBase->editor(), u); job->start(); } void ComposerDialog::slotPrint() { QPrinter printer; QPrintDialog dialog(&printer, this); dialog.setWindowTitle(i18nc("@title:window", "Print Document")); if (dialog.exec() != QDialog::Accepted) return; printInternal(&printer); } void ComposerDialog::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 ComposerDialog::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 ComposerDialog::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 ComposerDialog::slotWordWrapToggled(bool on) { if (on) { mComposerBase->editor()->enableWordWrap(validateLineWrapWidth()); } else { disableWordWrap(); } } int ComposerDialog::validateLineWrapWidth() const { int lineWrap = MessageComposer::MessageComposerSettings::self()->lineWrapWidth(); if ((lineWrap == 0) || (lineWrap > 78)) { lineWrap = 78; } else if (lineWrap < 30) { lineWrap = 30; } return lineWrap; } void ComposerDialog::disableWordWrap() { mComposerBase->editor()->disableWordWrap(); } void ComposerDialog::slotAutoSpellCheckingToggled(bool enabled) { mAutoSpellCheckingAction->setChecked(enabled); if (mComposerBase->editor()->checkSpellingEnabled() != enabled) { mComposerBase->editor()->setCheckSpellingEnabled(enabled); } //mStatusBarLabelSpellCheckingChangeMode->setToggleMode(enabled); } void ComposerDialog::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 ComposerDialog::closeEvent(QCloseEvent *event) +{ + event->ignore(); + ComposerWindowFactory::self().clear(this); +} + +bool ComposerDialog::queryClose() +{ + return KXmlGuiWindow::queryClose(); +} diff --git a/server/editor/composerdialog.h b/server/editor/composerdialog.h index 43c0fa2..6b91976 100644 --- a/server/editor/composerdialog.h +++ b/server/editor/composerdialog.h @@ -1,193 +1,213 @@ // 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 #include +#include #include #include -#include +#include +// App includes #include "encryptionstate.h" -#include "../identity/identitymanager.h" +#include "composerwindowfactory.h" class QSplitter; class QLabel; class QPrinter; class QGridLayout; class QLineEdit; class QPushButton; class KLineEdit; class RecipientsEditor; class KToggleAction; class CryptoStateIndicatorWidget; class RecipientLineNG; class NearExpiryWarning; class IdentityCombo; class KMComposerGlobalAction; class KRecentFilesAction; namespace KPIMTextEdit { class RichTextComposerWidget; class RichTextComposer; } namespace TextCustomEditor { class RichTextEditorWidget; } namespace MessageComposer { class ComposerViewBase; class RichTextComposerNg; } namespace Kleo { class KeyResolverCore; class ExpiryChecker; } class ComposerDialog : public KXmlGuiWindow { Q_OBJECT enum Confirmation { LetUserConfirm, NoConfirmationNeeded, }; public: - explicit ComposerDialog(const QString &fromAddress, const QString &name, const QByteArray &bearerToken, QWidget *parent = nullptr); - struct AttachmentInfo { QString comment; QUrl url; }; KIdentityManagementCore::Identity identity() 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 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(); +protected: + friend ComposerWindowFactory; + + explicit ComposerDialog(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, }; + /// Ask for confirmation if the message was changed. + [[nodiscard]] bool queryClose() override; + void annotateRecipientEditorLineWithCrpytoInfo(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 message will be signed. */ [[nodiscard]] bool sign() const; Kleo::CryptoMessageFormat cryptoMessageFormat() const; void updateSignatureAndEncryptionStateIndicators(); 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; CryptoStateIndicatorWidget *const mCryptoStateIndicatorWidget; MessageComposer::RichTextComposerNg *const mRichTextComposer; TextCustomEditor::RichTextEditorWidget *const mRichTextEditorWidget; NearExpiryWarning *const mNearExpiryWarning; KMComposerGlobalAction *const mGlobalAction; IdentityCombo * 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; EncryptionState mEncryptionState; KToggleAction *mMarkupAction = nullptr; std::shared_ptr mExpiryChecker; bool mIsModified = false; KRecentFilesAction *mRecentAction = nullptr; KToggleAction *mWordWrapAction = nullptr; KToggleAction *mAutoSpellCheckingAction = nullptr; + bool mLastSignActionState = false; + std::shared_ptr mKeyCache; + bool mLastEncryptActionState = false; }; diff --git a/server/editor/encryptionstate.h b/server/editor/encryptionstate.h index 8254662..bdea786 100644 --- a/server/editor/encryptionstate.h +++ b/server/editor/encryptionstate.h @@ -1,120 +1,129 @@ -/* - SPDX-FileCopyrightText: 2022 Sandro Knauß - SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL -*/ +// SPDX-FileCopyrightText: 2022 Sandro Knauß +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #pragma once #include -/** - * @todo write docs - */ +/// Encryption state +/// +/// This store whether the message will be encrypted or not. class EncryptionState : public QObject { Q_OBJECT Q_PROPERTY(bool override READ override WRITE setOverride NOTIFY overrideChanged) Q_PROPERTY(bool possibleEncrypt READ possibleEncrypt WRITE setPossibleEncrypt NOTIFY possibleEncryptChanged) + + /// This property holds whether the user want auto encryption. + /// \see autoEncrypt + /// \see setAutoEncrypt + /// \see autoEncryptChanged Q_PROPERTY(bool autoEncrypt READ autoEncrypt WRITE setAutoEncrypt NOTIFY autoEncryptChanged) + + /// This property holds whether keys were found for all the recipients. + /// \see acceptedSolution + /// \see setAcceptedSolution + /// \see acceptedSolutionChanged Q_PROPERTY(bool acceptedSolution READ acceptedSolution WRITE setAcceptedSolution NOTIFY acceptedSolutionChanged) + + /// This property holds whether we encrypt the message. + /// \see encrypt + /// \see encryptChanged Q_PROPERTY(bool encrypt READ encrypt NOTIFY encryptChanged) public: /** * Default constructor */ EncryptionState(); /** * @return the user set the encryption state no matter what */ [[nodiscard]] bool override() const; /** * @return true when set an override */ [[nodiscard]] bool hasOverride() const; /** * @return we have encryption keys for the user so in principal it is possible to encrypt */ [[nodiscard]] bool possibleEncrypt() const; - /** - * @return the user wants auto encryption - */ [[nodiscard]] bool autoEncrypt() const; /** * @return we found a set of keys to encrypt to everyone */ [[nodiscard]] bool acceptedSolution() const; /** * @return the encrypt */ [[nodiscard]] bool encrypt() const; public Q_SLOTS: /** * Sets the override. * * @param override the new override */ void setOverride(bool override); /** * Delete the override. */ void unsetOverride(); /** * Toggles the override */ void toggleOverride(); /** * Sets the acceptedSolution. * * @param acceptedSolution the new acceptedSolution */ void setAcceptedSolution(bool acceptedSolution); /** * Sets the possibleEncrypt. * * @param possibleEncrypt the new possibleEncrypt */ void setPossibleEncrypt(bool possibleEncrypt); /** * Sets the autoEncrypt. * * @param autoEncrypt the new autoEncrypt */ void setAutoEncrypt(bool autoEncrypt); Q_SIGNALS: void overrideChanged(bool override); void hasOverrideChanged(bool hasOverride); void acceptedSolutionChanged(bool acceptedSolution); void possibleEncryptChanged(bool possibleEncrypt); void autoEncryptChanged(bool autoEncrypt); void encryptChanged(bool encrypt); private: void setEncrypt(bool encrypt); void updateEncrypt(); private: bool m_override = false; bool m_hasOverride = false; bool m_acceptedSolution = false; bool m_possibleEncrypt = false; bool m_autoEncrypt = false; bool m_encrypt = false; }; diff --git a/server/editor/mailtemplates.cpp b/server/editor/mailtemplates.cpp index 8f70dc8..2c3c3df 100644 --- a/server/editor/mailtemplates.cpp +++ b/server/editor/mailtemplates.cpp @@ -1,972 +1,972 @@ /* 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 using namespace Qt::Literals::StringLiterals; QDebug operator<<(QDebug dbg, const KMime::Types::Mailbox &mb) { dbg << mb.addrSpec().asString(); return dbg; } 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(); auto message = new Message(); message->setContent(encoded); message->parse(); 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() << "Error: " << rx.errorString() << rx; Q_ASSERT(false); return str; } QString tmp = str; //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 return newPrefix + u' ' + str; } const QStringList getForwardPrefixes() { //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 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 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 QStringList list; //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) 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()); 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->addContent(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()); htmlPart->contentType()->setCharset("utf-8"); htmlPart->contentTransferEncoding()->setEncoding(KMime::Headers::CE8Bit); htmlPart->fromUnicodeString(htmlBody); multipartAlternative->addContent(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->addContent(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 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 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)) { posNewLine = res.indexOf(QLatin1Char('\n'), posNewLine) + 1; } else { 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); } static QString extractHeaderBodyScript() { return QStringLiteral("(function() {" "var res = {" " body: document.getElementsByTagName('body')[0].innerHTML," " header: document.getElementsByTagName('head')[0].innerHTML" "};" "return res;" "})()"); } void htmlMessageText(const QString &plainTextContent, const QString &htmlContent, const std::function &callback) { QString htmlElement = htmlContent; 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; 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()); // } 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. //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 }; 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 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) { KMime::Types::Mailbox::List toList; KMime::Types::Mailbox::List ccList; 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) { 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) { //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 {}; }; 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 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 QString plainBody; QString htmlBody; //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)).arg(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) { QString result = stripSignature(body); //Quoted body result = quotedPlainText(result, fromHeader ? fromHeader->displayString() : QString{}); if (result.endsWith(u'\n')) { result.chop(1); } //The plain body is complete auto plainBodyResult = plainBody + result; htmlMessageText(plainTextContent, htmlContent, [=] (const QString &body, const QString &headElement) { QString result = stripSignature(body); //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 msg->contentType()->clear(); // to get rid of old boundary 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. msg->assemble(); callback(msg); }); }); } 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); KMime::Message::Ptr tmpForwardedMessage; auto attachments = otp.collectAttachmentParts(); if (!attachments.isEmpty()) { QVector contents = {recreatedMsg}; for (const auto &attachment : attachments) { //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())) ); 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->addContent(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 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::Content *createAttachmentPart(const QByteArray &content, const QString &filename, bool isInline, const QByteArray &mimeType, const QString &name, bool base64Encode = true) { KMime::Content *part = new KMime::Content; part->contentDisposition(true)->setFilename(filename); if (isInline) { part->contentDisposition(true)->setDisposition(KMime::Headers::CDinline); } else { part->contentDisposition(true)->setDisposition(KMime::Headers::CDattachment); } part->contentType(true)->setMimeType(mimeType); if (!name.isEmpty()) { part->contentType(true)->setName(name, "utf-8"); } if(base64Encode) { part->contentTransferEncoding(true)->setEncoding(KMime::Headers::CEbase64); } part->setBody(content); return part; } static KMime::Content *createBodyPart(const QString &body, bool htmlBody) { if (htmlBody) { return createMultipartAlternativeContent(toPlainText(body), body); } return createPlainPartContent(body); } 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(); for (const auto &mb : stringListToMailboxes(recipients.to)) { message.to()->addAddress(mb); } message.cc(true)->clear(); for (const auto &mb : stringListToMailboxes(recipients.cc)) { message.cc()->addAddress(mb); } message.bcc(true)->clear(); for (const auto &mb : stringListToMailboxes(recipients.bcc)) { 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 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->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::CEquPr); + bodyMessage->contentTransferEncoding()->setEncoding(KMime::Headers::CEbase64); bodyMessage->setBody(KMime::CRLFtoLF(body.toUtf8())); - message->addContent( bodyMessage ); + 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::CEquPr); + attachMessage->contentTransferEncoding()->setEncoding(KMime::Headers::CEbase64); attachMessage->setBody(KMime::CRLFtoLF(attachment.toUtf8())); - message->addContent(attachMessage); + message->appendContent(attachMessage); // Job done, attach the both multiparts and assemble the message. message->assemble(); return message; } diff --git a/server/identity/identitymanager.h b/server/identity/identitymanager.h index a1f9198..8c4302d 100644 --- a/server/identity/identitymanager.h +++ b/server/identity/identitymanager.h @@ -1,39 +1,40 @@ // SPDX-FileCopyrightText: 2023 g10 code Gmbh // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include class KConfig; class IdentityManager : public QObject { Q_OBJECT public: static IdentityManager &self(); KIdentityManagementCore::Identity fromEmail(const QString &email, bool &isNew); KIdentityManagementCore::Identity fromUoid(uint uid); void updateIdentity(KIdentityManagementCore::Identity identity); KIdentityManagementCore::Identity::List identities() const; QStringList identityEmails() const; + void writeConfig() const; + void readConfig(KConfig *config); + Q_SIGNALS: void identitiesWereChanged(); private: - void writeConfig() const; - void readConfig(KConfig *config); QStringList groupList(KConfig *config) const; IdentityManager(); ~IdentityManager(); KIdentityManagementCore::Identity::List mIdentities; KConfig *mConfig = nullptr; }; \ No newline at end of file