diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 53ec7dc..cf4ecf4 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -1,283 +1,289 @@ # SPDX-FileCopyrightText: 2023 g10 code GmbH # SPDX-Contributor: Carl Schwan # SPDX-License-Identifier: BSD-2-Clause add_library(gpgol-server-static STATIC) target_sources(gpgol-server-static PRIVATE websocketclient.cpp websocketclient.h webserver.h webserver.cpp # Identity identity/addressvalidationjob.cpp identity/addressvalidationjob.h identity/identitymanager.cpp identity/identitymanager.h identity/identitydialog.cpp identity/identitydialog.h + identity/identity.cpp + identity/identity.h + identity/signature.h + identity/signature.cpp + identity/signatureconfigurator.cpp + identity/signatureconfigurator.h + identity/signaturerichtexteditor.cpp + identity/signaturerichtexteditor_p.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/addresseelineedit.cpp editor/addresseelineedit.h editor/addresseelineeditmanager.cpp editor/addresseelineeditmanager.h editor/composer.cpp editor/composer.h editor/composerviewbase.cpp editor/composerviewbase.h editor/composerwindow.cpp editor/composerwindow.h editor/composerwindowfactory.cpp editor/composerwindowfactory.h editor/cryptostateindicatorwidget.cpp editor/cryptostateindicatorwidget.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/kmailcompletion.cpp editor/kmailcompletion.h 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 ) qt_add_resources(gpgol-server-static PREFIX "/" FILES assets/certificate.crt ) 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 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 common Qt6::HttpServer Qt6::Widgets Qt6::PrintSupport KF6::JobWidgets 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::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) if (BUILD_TESTING) add_subdirectory(autotests) endif() diff --git a/server/editor/attachment/attachmentcontroller.cpp b/server/editor/attachment/attachmentcontroller.cpp index 31616e6..b1a770c 100644 --- a/server/editor/attachment/attachmentcontroller.cpp +++ b/server/editor/attachment/attachmentcontroller.cpp @@ -1,128 +1,128 @@ /* * This file is part of KMail. * SPDX-FileCopyrightText: 2009 Constantin Berzan * * Parts based on KMail code by: * Various authors. * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "attachmentcontroller.h" #include "attachmentview.h" #include "attachmentmodel.h" #include "attachmentpart.h" #include "attachmentview.h" #include "editor/composerwindow.h" #include "editor_debug.h" #include #include -#include +#include "identity/identity.h" #include #include #include using namespace MessageCore; AttachmentController::AttachmentController(MessageComposer::AttachmentModel *model, AttachmentView *view, ComposerWindow *composer) : AttachmentControllerBase(model, composer, composer->actionCollection()) , mComposer(composer) , mView(view) { connect(composer, &ComposerWindow::identityChanged, this, &AttachmentController::identityChanged); connect(view, &AttachmentView::contextMenuRequested, this, &AttachmentControllerBase::showContextMenu); connect(view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &AttachmentController::selectionChanged); connect(view, &QAbstractItemView::doubleClicked, this, &AttachmentController::doubleClicked); connect(this, &AttachmentController::refreshSelection, this, &AttachmentController::selectionChanged); connect(this, &AttachmentController::showAttachment, this, &AttachmentController::onShowAttachment); connect(this, &AttachmentController::selectedAllAttachment, this, &AttachmentController::slotSelectAllAttachment); connect(this, &AttachmentController::actionsCreated, this, &AttachmentController::slotActionsCreated); } AttachmentController::~AttachmentController() = default; void AttachmentController::slotSelectAllAttachment() { mView->selectAll(); } void AttachmentController::identityChanged() { const KIdentityManagementCore::Identity &identity = mComposer->identity(); // "Attach public key" is only possible if OpenPGP support is available: enableAttachPublicKey(QGpgME::openpgp()); // "Attach my public key" is only possible if OpenPGP support is // available and the user specified his key for the current identity: enableAttachMyPublicKey(QGpgME::openpgp() && !identity.pgpEncryptionKey().isEmpty()); } void AttachmentController::attachMyPublicKey() { const KIdentityManagementCore::Identity &identity = mComposer->identity(); qCDebug(EDITOR_LOG) << identity.identityName(); auto keyCache = Kleo::KeyCache::instance(); auto key = keyCache->findByFingerprint(identity.pgpEncryptionKey().data()); exportPublicKey(key); } void AttachmentController::slotActionsCreated() { // Disable public key actions if appropriate. identityChanged(); // Disable actions like 'Remove', since nothing is currently selected. selectionChanged(); } void AttachmentController::selectionChanged() { const QModelIndexList selectedRows = mView->selectionModel()->selectedRows(); AttachmentPart::List selectedParts; selectedParts.reserve(selectedRows.count()); for (const QModelIndex &index : selectedRows) { auto part = mView->model()->data(index, MessageComposer::AttachmentModel::AttachmentPartRole).value(); selectedParts.append(part); } setSelectedParts(selectedParts); } void AttachmentController::onShowAttachment(KMime::Content *content, const QByteArray &charset) { const QString charsetStr = QString::fromLatin1(charset); if (content->bodyAsMessage()) { KMime::Message::Ptr message(new KMime::Message); message->setContent(content->bodyAsMessage()->encodedContent()); message->parse(); auto dialog = new MimeTreeParser::Widgets::MessageViewerDialog({ message }); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->show(); } } void AttachmentController::doubleClicked(const QModelIndex &itemClicked) { if (!itemClicked.isValid()) { qCDebug(EDITOR_LOG) << "Received an invalid item clicked index"; return; } // The itemClicked index will contain the column information. But we want to retrieve // the AttachmentPart, so we must recreate the QModelIndex without the column information const QModelIndex &properItemClickedIndex = mView->model()->index(itemClicked.row(), 0); auto part = mView->model()->data(properItemClickedIndex, MessageComposer::AttachmentModel::AttachmentPartRole).value(); // We can't edit encapsulated messages, but we can view them. if (part->isMessageOrMessageCollection()) { viewAttachment(part); } } #include "moc_attachmentcontroller.cpp" diff --git a/server/editor/composerviewbase.cpp b/server/editor/composerviewbase.cpp index 86c8ab4..358c1aa 100644 --- a/server/editor/composerviewbase.cpp +++ b/server/editor/composerviewbase.cpp @@ -1,1352 +1,1352 @@ /* SPDX-FileCopyrightText: 2010 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.com SPDX-FileCopyrightText: 2010 Leo Franchi SPDX-License-Identifier: LGPL-2.0-or-later */ #include "composerviewbase.h" #include "attachment/attachmentcontrollerbase.h" #include "attachment/attachmentmodel.h" #include "richtextcomposerng.h" #include "richtextcomposersignatures.h" #include "composer.h" #include "nodehelper.h" #include "signaturecontroller.h" #include "part/globalpart.h" #include "part/infopart.h" #include "util.h" #include "util_p.h" #include "ews/ewsmailfactory.h" #include "mailtemplates.h" #include "../qnam.h" #include "messagecomposersettings.h" #include "recipientseditor.h" #include -#include +#include "identity/identity.h" #include #include #include #include #include #include "editor_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "draft/draftmanager.h" using namespace MessageComposer; using namespace Qt::Literals::StringLiterals; ComposerViewBase::ComposerViewBase(QObject *parent, QWidget *parentGui) : QObject(parent) , m_msg(KMime::Message::Ptr(new KMime::Message)) , m_parentWidget(parentGui) , m_cryptoMessageFormat(Kleo::AutoFormat) , m_autoSaveInterval(60000) // default of 1 min { m_charsets << "utf-8"; // default, so we have a backup in case client code forgot to set. initAutoSave(); connect(this, &ComposerViewBase::composerCreated, this, &ComposerViewBase::slotComposerCreated); } ComposerViewBase::~ComposerViewBase() = default; bool ComposerViewBase::isComposing() const { return !m_composers.isEmpty(); } void ComposerViewBase::setMessage(const KMime::Message::Ptr &msg) { if (m_attachmentModel) { const auto attachments{m_attachmentModel->attachments()}; for (const MessageCore::AttachmentPart::Ptr &attachment : attachments) { if (!m_attachmentModel->removeAttachment(attachment)) { qCWarning(EDITOR_LOG) << "Attachment not found."; } } } m_msg = msg; if (m_recipientsEditor) { m_recipientsEditor->clear(); bool resultTooManyRecipients = m_recipientsEditor->setRecipientString(m_msg->to()->mailboxes(), Recipient::To); if (!resultTooManyRecipients) { resultTooManyRecipients = m_recipientsEditor->setRecipientString(m_msg->cc()->mailboxes(), Recipient::Cc); } if (!resultTooManyRecipients) { resultTooManyRecipients = m_recipientsEditor->setRecipientString(m_msg->bcc()->mailboxes(), Recipient::Bcc); } if (!resultTooManyRecipients) { resultTooManyRecipients = m_recipientsEditor->setRecipientString(m_msg->replyTo()->mailboxes(), Recipient::ReplyTo); } m_recipientsEditor->setFocusBottom(); Q_EMIT tooManyRecipient(resultTooManyRecipients); } // First, we copy the message and then parse it to the object tree parser. // The otp gets the message text out of it, in textualContent(), and also decrypts // the message if necessary. auto msgContent = new KMime::Content; msgContent->setContent(m_msg->encodedContent()); msgContent->parse(); // Load the attachments const auto attachments{msgContent->attachments()}; for (const auto &att : attachments) { addAttachmentPart(att); } // Set the HTML text and collect HTML images bool isHtml = false; const auto body = MailTemplates::body(msg, isHtml); if (isHtml) { enableHtml(); } else { disableHtml(LetUserConfirm); } editor()->setText(body); if (auto hdr = m_msg->headerByType("X-KMail-CursorPos")) { m_editor->setCursorPositionFromStart(hdr->asUnicodeString().toUInt()); } delete msgContent; } void ComposerViewBase::saveMailSettings() { auto header = new KMime::Headers::Generic("X-KMail-Identity"); header->fromUnicodeString(QString::number(m_identity.uoid()), "utf-8"); m_msg->setHeader(header); header = new KMime::Headers::Generic("X-KMail-Identity-Name"); header->fromUnicodeString(m_identity.identityName(), "utf-8"); m_msg->setHeader(header); header = new KMime::Headers::Generic("X-KMail-Dictionary"); header->fromUnicodeString(m_dictionary->currentDictionary(), "utf-8"); m_msg->setHeader(header); // Save the quote prefix which is used for this message. Each message can have // a different quote prefix, for example depending on the original sender. if (m_editor->quotePrefixName().isEmpty()) { m_msg->removeHeader("X-KMail-QuotePrefix"); } else { header = new KMime::Headers::Generic("X-KMail-QuotePrefix"); header->fromUnicodeString(m_editor->quotePrefixName(), "utf-8"); m_msg->setHeader(header); } if (m_editor->composerControler()->isFormattingUsed()) { qCDebug(EDITOR_LOG) << "HTML mode"; header = new KMime::Headers::Generic("X-KMail-Markup"); header->fromUnicodeString(QStringLiteral("true"), "utf-8"); m_msg->setHeader(header); } else { m_msg->removeHeader("X-KMail-Markup"); qCDebug(EDITOR_LOG) << "Plain text"; } } void ComposerViewBase::clearFollowUp() { mFollowUpDate = QDate(); } void ComposerViewBase::send() { KCursorSaver saver(Qt::WaitCursor); saveMailSettings(); if (m_editor->composerControler()->isFormattingUsed() && inlineSigningEncryptionSelected()) { const QString keepBtnText = m_encrypt ? m_sign ? i18n("&Keep markup, do not sign/encrypt") : i18n("&Keep markup, do not encrypt") : i18n("&Keep markup, do not sign"); const QString yesBtnText = m_encrypt ? m_sign ? i18n("Sign/Encrypt (delete markup)") : i18n("Encrypt (delete markup)") : i18n("Sign (delete markup)"); int ret = KMessageBox::warningTwoActionsCancel(m_parentWidget, i18n("

Inline signing/encrypting of HTML messages is not possible;

" "

do you want to delete your markup?

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

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

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

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

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

" "

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

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

" "Select the certificate(s) which should " "be used for this recipient. If there is no suitable certificate in the list " "you can also search for external certificates by clicking the button: " "search for external certificates.
", recipient->name().isEmpty() ? recipient->email() : recipient->name()); const bool opgp = containsOpenPGP(cryptoMessageFormat()); const bool x509 = containsSMIME(cryptoMessageFormat()); QPointer dlg = new Kleo::KeySelectionDialog( i18n("Encryption Key Selection"), msg, recipient->email(), {}, Kleo::KeySelectionDialog::ValidEncryptionKeys | (opgp ? Kleo::KeySelectionDialog::OpenPGPKeys : 0) | (x509 ? Kleo::KeySelectionDialog::SMIMEKeys : 0), false, // multi-selection false); // "remember choice" box; dlg->open(); connect(dlg, &QDialog::accepted, this, [dlg, recipient, line, this]() { auto key = dlg->selectedKey(); key.update(); // We need tofu information for key. recipient->setKey(key); annotateRecipientEditorLineWithCryptoInfo(line); }); } void ComposerWindow::slotRecipientEditorFocusChanged() { if (!mEncryptAction->isChecked()) { return; } if (mKeyCache->initialized()) { mRunKeyResolverTimer->stop(); runKeyResolver(); } } void ComposerWindow::slotRecipientAdded(RecipientLineNG *line) { if (line->recipientsCount() == 0) { return; } if (!mKeyCache->initialized()) { if (line->property("keyLookupJob").toBool()) { return; } line->setProperty("keyLookupJob", true); // We need to start key listing on our own othweise KMail will crash and we want to wait till the cache is populated. connect(mKeyCache.get(), &Kleo::KeyCache::keyListingDone, this, [this, line]() { slotRecipientAdded(line); }); return; } if (mKeyCache->initialized()) { mRunKeyResolverTimer->start(); } } void ComposerWindow::slotRecipientFocusLost(RecipientLineNG *line) { if (line->recipientsCount() == 0) { return; } if (mKeyCache->initialized()) { mRunKeyResolverTimer->start(); } } diff --git a/server/editor/composerwindow.h b/server/editor/composerwindow.h index 28bceaf..a1e65bb 100644 --- a/server/editor/composerwindow.h +++ b/server/editor/composerwindow.h @@ -1,225 +1,225 @@ // 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 "identity/identity.h" // App includes #include "composerwindowfactory.h" class QSplitter; class QLabel; class QPrinter; class QGridLayout; class QLineEdit; class QPushButton; class KLineEdit; class RecipientsEditor; class KToggleAction; class RecipientLineNG; class NearExpiryWarning; class IdentityCombo; class KMComposerGlobalAction; class KRecentFilesAction; namespace KPIMTextEdit { class RichTextComposerWidget; class RichTextComposer; } namespace TextCustomEditor { class RichTextEditorWidget; } namespace MessageComposer { class ComposerViewBase; class RichTextComposerNg; } namespace Kleo { class KeyResolverCore; class ExpiryChecker; } class ComposerWindow : public KXmlGuiWindow { Q_OBJECT enum Confirmation { LetUserConfirm, NoConfirmationNeeded, }; public: struct AttachmentInfo { QString comment; QUrl url; }; /// The identity assigned to this message. KIdentityManagementCore::Identity identity() const; /// The subject of the message. QString subject() const; /// The recipients of the message. RecipientsEditor *recipientsEditor() const; /// The content of the message. QString content() const; void addAttachment(const QList &infos, bool showWarning); void reply(const KMime::Message::Ptr &message); void forward(const KMime::Message::Ptr &message); void setMessage(const KMime::Message::Ptr &message); private Q_SLOTS: void slotSend(); void slotToggleMarkup(); void slotSignToggled(bool on); void slotSaveDraft(); void slotSaveAsFile(); void slotInsertFile(); void slotEncryptionButtonIconUpdate(); void slotEditIdentity(); void slotIdentityChanged(); void slotPrint(); void slotPrintPreview(); void slotWordWrapToggled(bool on); void slotAutoSpellCheckingToggled(bool enabled); void slotSpellcheckConfig(); void printInternal(QPrinter *printer); void enableHtml(); void slotPasteAsAttachment(); void disableHtml(Confirmation confirmation); void slotCursorPositionChanged(); void slotInsertRecentFile(const QUrl &u); void slotRecentListFileClear(); void slotRecipientEditorFocusChanged(); void slotRecipientAdded(RecipientLineNG *line); void slotRecipientFocusLost(RecipientLineNG *line); void slotRecipientEditorLineAdded(RecipientLineNG *line); void slotRecipientLineIconClicked(RecipientLineNG *line); void insertUrls(const QMimeData *source, const QList &urlList); bool insertFromMimeData(const QMimeData *source, bool forceAttachment); QUrl insertFile(); void addAttachment(const QString &name, KMime::Headers::contentEncoding cte, const QString &charset, const QByteArray &data, const QByteArray &mimeType); /// Set whether the message will be signed. void setSigning(bool sign, bool setByUser = false); /// Set whether the message should be treated as modified or not. void setModified(bool modified); std::shared_ptr expiryChecker(); Q_SIGNALS: void identityChanged(); void initialized(); protected: friend ComposerWindowFactory; explicit ComposerWindow(const QString &fromAddress, const QString &name, const QByteArray &bearerToken, QWidget *parent = nullptr); void reset(const QString &fromAddress, const QString &name, const QByteArray &bearerToken); void closeEvent(QCloseEvent *event) override; private: enum CryptoKeyState { NoState = 0, InProgress, KeyOk, NoKey, }; /// Ask for confirmation if the message was changed. [[nodiscard]] bool queryClose() override; void annotateRecipientEditorLineWithCryptoInfo(RecipientLineNG *line); void setupActions(); void setupStatusBar(QWidget *w); void htmlToolBarVisibilityChanged(bool visible); void changeCryptoAction(); void runKeyResolver(); int validateLineWrapWidth() const; void disableWordWrap(); void checkOwnKeyExpiry(const KIdentityManagementCore::Identity &ident); std::unique_ptr fillKeyResolver(); /** * Returns true if the message was modified by the user. */ [[nodiscard]] bool isModified() const; /** * Returns true if the message will be signed. */ [[nodiscard]] bool sign() const; Kleo::CryptoMessageFormat cryptoMessageFormat() const; KIdentityManagementCore::Identity mIdentity; QString mFrom; QWidget *const mMainWidget; // splitter between the headers area and the actual editor MessageComposer::ComposerViewBase *const mComposerBase; QSplitter *const mHeadersToEditorSplitter; QWidget *const mHeadersArea; QGridLayout *const mGrid; QLabel *const mLblFrom; QPushButton *const mButtonFrom; RecipientsEditor *const mRecipientEditor; QLabel *const mLblSubject; QLineEdit *const mEdtSubject; MessageComposer::RichTextComposerNg *const mRichTextComposer; TextCustomEditor::RichTextEditorWidget *const mRichTextEditorWidget; NearExpiryWarning *const mNearExpiryWarning; KMComposerGlobalAction *const mGlobalAction; QLabel *mEdtFrom = nullptr; bool mForceDisableHtml = false; bool mLastIdentityHasSigningKey = false; bool mLastIdentityHasEncryptionKey = false; QAction *mEncryptAction = nullptr; QAction *mSignAction = nullptr; QAction *mAppendSignature = nullptr; QAction *mPrependSignature = nullptr; QAction *mInsertSignatureAtCursorPosition = nullptr; QAction *mSelectAll = nullptr; QAction *mFindText = nullptr; QAction *mFindNextText = nullptr; QAction *mReplaceText = nullptr; QLabel *mStatusbarLabel = nullptr; QLabel *mCursorLineLabel = nullptr; QLabel *mCursorColumnLabel = nullptr; KToggleAction *mMarkupAction = nullptr; std::shared_ptr mExpiryChecker; bool mIsModified = false; bool mAcceptedSolution = false; KRecentFilesAction *mRecentAction = nullptr; KToggleAction *mWordWrapAction = nullptr; KToggleAction *mAutoSpellCheckingAction = nullptr; bool mLastSignActionState = false; std::shared_ptr mKeyCache; bool mLastEncryptActionState = false; QTimer *mRunKeyResolverTimer = nullptr; }; diff --git a/server/editor/richtextcomposerng.h b/server/editor/richtextcomposerng.h index 31e4072..24cbc49 100644 --- a/server/editor/richtextcomposerng.h +++ b/server/editor/richtextcomposerng.h @@ -1,57 +1,57 @@ /* SPDX-FileCopyrightText: 2015-2023 Laurent Montel SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once -#include +#include "identity/signature.h" #include class RichTextComposerNgTest; namespace TextAutoCorrectionCore { class AutoCorrection; } namespace MessageComposer { class TextPart; class RichTextComposerSignatures; class RichTextComposerNgPrivate; /** * @brief The RichTextComposerNg class * @author Laurent Montel */ class RichTextComposerNg : public KPIMTextEdit::RichTextComposer { Q_OBJECT public: explicit RichTextComposerNg(QWidget *parent = nullptr); ~RichTextComposerNg() override; [[nodiscard]] TextAutoCorrectionCore::AutoCorrection *autocorrection() const; void setAutocorrection(TextAutoCorrectionCore::AutoCorrection *autocorrect); void setAutocorrectionLanguage(const QString &lang); void fillComposerTextPart(MessageComposer::TextPart *textPart); [[nodiscard]] MessageComposer::RichTextComposerSignatures *composerSignature() const; void insertSignature(const KIdentityManagementCore::Signature &signature, KIdentityManagementCore::Signature::Placement placement, KIdentityManagementCore::Signature::AddedText addedText); [[nodiscard]] QString toCleanHtml() const; void forceAutoCorrection(bool selectedText = false) override; protected: bool processModifyText(QKeyEvent *event) override; private: std::unique_ptr const d; friend class ::RichTextComposerNgTest; // for fixHtmlFontSize void fixHtmlFontSize(QString &cleanHtml) const; }; } diff --git a/server/editor/richtextcomposersignatures.cpp b/server/editor/richtextcomposersignatures.cpp index 245c1dc..3b18239 100644 --- a/server/editor/richtextcomposersignatures.cpp +++ b/server/editor/richtextcomposersignatures.cpp @@ -1,178 +1,178 @@ /* SPDX-FileCopyrightText: 2015-2023 Laurent Montel SPDX-License-Identifier: GPL-2.0-or-later */ #include "richtextcomposersignatures.h" #include "richtextcomposerng.h" -#include +#include "identity/signature.h" #include #include using namespace MessageComposer; class RichTextComposerSignatures::RichTextComposerSignaturesPrivate { public: RichTextComposerSignaturesPrivate(RichTextComposerNg *composer) : richTextComposer(composer) { } void cleanWhitespaceHelper(const QRegularExpression ®Exp, const QString &newText, const KIdentityManagementCore::Signature &sig); [[nodiscard]] QList> signaturePositions(const KIdentityManagementCore::Signature &sig) const; RichTextComposerNg *const richTextComposer; }; RichTextComposerSignatures::RichTextComposerSignatures(MessageComposer::RichTextComposerNg *composer, QObject *parent) : QObject(parent) , d(new RichTextComposerSignaturesPrivate(composer)) { } RichTextComposerSignatures::~RichTextComposerSignatures() = default; void RichTextComposerSignatures::RichTextComposerSignaturesPrivate::cleanWhitespaceHelper(const QRegularExpression ®Exp, const QString &newText, const KIdentityManagementCore::Signature &sig) { int currentSearchPosition = 0; for (;;) { // Find the text const QString text = richTextComposer->document()->toPlainText(); const auto currentMatch = regExp.match(text, currentSearchPosition); if (!currentMatch.hasMatch()) { break; } currentSearchPosition = currentMatch.capturedStart(); // Select the text QTextCursor cursor(richTextComposer->document()); cursor.setPosition(currentSearchPosition); cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, currentMatch.capturedLength()); // Skip quoted text if (richTextComposer->isLineQuoted(cursor.block().text())) { currentSearchPosition += currentMatch.capturedLength(); continue; } // Skip text inside signatures bool insideSignature = false; const QList> sigPositions = signaturePositions(sig); for (const QPair &position : sigPositions) { if (cursor.position() >= position.first && cursor.position() <= position.second) { insideSignature = true; } } if (insideSignature) { currentSearchPosition += currentMatch.capturedLength(); continue; } // Replace the text cursor.removeSelectedText(); cursor.insertText(newText); currentSearchPosition += newText.length(); } } void RichTextComposerSignatures::cleanWhitespace(const KIdentityManagementCore::Signature &sig) { QTextCursor cursor(d->richTextComposer->document()); cursor.beginEditBlock(); // Squeeze tabs and spaces d->cleanWhitespaceHelper(QRegularExpression(QLatin1String("[\t ]+")), QStringLiteral(" "), sig); // Remove trailing whitespace d->cleanWhitespaceHelper(QRegularExpression(QLatin1String("[\t ][\n]")), QStringLiteral("\n"), sig); // Single space lines d->cleanWhitespaceHelper(QRegularExpression(QLatin1String("[\n]{3,}")), QStringLiteral("\n\n"), sig); if (!d->richTextComposer->textCursor().hasSelection()) { d->richTextComposer->textCursor().clearSelection(); } cursor.endEditBlock(); } QList> RichTextComposerSignatures::RichTextComposerSignaturesPrivate::signaturePositions(const KIdentityManagementCore::Signature &sig) const { QList> signaturePositions; if (!sig.rawText().isEmpty()) { QString sigText = sig.toPlainText(); int currentSearchPosition = 0; for (;;) { // Find the next occurrence of the signature text const QString text = richTextComposer->document()->toPlainText(); const int currentMatch = text.indexOf(sigText, currentSearchPosition); currentSearchPosition = currentMatch + sigText.length(); if (currentMatch == -1) { break; } signaturePositions.append(QPair(currentMatch, currentMatch + sigText.length())); } } return signaturePositions; } bool RichTextComposerSignatures::replaceSignature(const KIdentityManagementCore::Signature &oldSig, const KIdentityManagementCore::Signature &newSig) { bool found = false; if (oldSig == newSig) { return false; } QString oldSigText = oldSig.toPlainText(); if (oldSigText.isEmpty()) { return false; } QTextCursor cursor(d->richTextComposer->document()); cursor.beginEditBlock(); int currentSearchPosition = 0; for (;;) { // Find the next occurrence of the signature text const QString text = d->richTextComposer->document()->toPlainText(); const int currentMatch = text.indexOf(oldSigText, currentSearchPosition); currentSearchPosition = currentMatch; if (currentMatch == -1) { break; } // Select the signature cursor.setPosition(currentMatch); // If the new signature is completely empty, we also want to remove the // signature separator, so include it in the selection int additionalMove = 0; if (newSig.rawText().isEmpty() && text.mid(currentMatch - 4, 4) == QLatin1String("-- \n")) { cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::MoveAnchor, 4); additionalMove = 4; } else if (newSig.rawText().isEmpty() && text.mid(currentMatch - 1, 1) == QLatin1Char('\n')) { cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::MoveAnchor, 1); additionalMove = 1; } cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, oldSigText.length() + additionalMove); // Skip quoted signatures if (d->richTextComposer->isLineQuoted(cursor.block().text())) { currentSearchPosition += oldSig.toPlainText().length(); continue; } // Remove the old and insert the new signature cursor.removeSelectedText(); d->richTextComposer->setTextCursor(cursor); d->richTextComposer->insertSignature(newSig, KIdentityManagementCore::Signature::AtCursor, KIdentityManagementCore::Signature::AddNothing); found = true; currentSearchPosition += newSig.toPlainText().length(); } cursor.endEditBlock(); return found; } #include "moc_richtextcomposersignatures.cpp" diff --git a/server/editor/signaturecontroller.cpp b/server/editor/signaturecontroller.cpp index 08edd50..15dc49c 100644 --- a/server/editor/signaturecontroller.cpp +++ b/server/editor/signaturecontroller.cpp @@ -1,117 +1,117 @@ /* * SPDX-FileCopyrightText: 2010 Volker Krause * * Based on kmail/kmcomposewin.cpp * SPDX-FileCopyrightText: 2009 Constantin Berzan * * Based on KMail code by: * SPDX-FileCopyrightText: 1997 Markus Wuebben * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "signaturecontroller.h" #include "richtextcomposerng.h" #include "richtextcomposersignatures.h" #include "messagecomposersettings.h" -#include +#include "identity/identity.h" using namespace MessageComposer; class MessageComposer::SignatureControllerPrivate { public: SignatureControllerPrivate() = default; KIdentityManagementCore::Identity m_currentIdentity; KIdentityManagementWidgets::IdentityCombo *m_identityCombo = nullptr; MessageComposer::RichTextComposerNg *m_editor = nullptr; }; SignatureController::SignatureController(QObject *parent) : QObject(parent) , d(new MessageComposer::SignatureControllerPrivate) { } SignatureController::~SignatureController() = default; void SignatureController::setEditor(MessageComposer::RichTextComposerNg *editor) { d->m_editor = editor; } void SignatureController::setIdentity(const KIdentityManagementCore::Identity &identity) { d->m_currentIdentity = identity; } void SignatureController::appendSignature() { insertSignatureHelper(KIdentityManagementCore::Signature::End); } void SignatureController::prependSignature() { insertSignatureHelper(KIdentityManagementCore::Signature::Start); } void SignatureController::insertSignatureAtCursor() { insertSignatureHelper(KIdentityManagementCore::Signature::AtCursor); } void SignatureController::cleanSpace() { if (!d->m_editor || d->m_currentIdentity.isNull()) { return; } const KIdentityManagementCore::Signature signature = d->m_currentIdentity.signature(); d->m_editor->composerSignature()->cleanWhitespace(signature); } void SignatureController::insertSignatureHelper(KIdentityManagementCore::Signature::Placement placement) { if (!d->m_editor || d->m_currentIdentity.isNull()) { return; } // Identity::signature() is not const, although it should be, therefore the // const_cast. const KIdentityManagementCore::Signature signature = d->m_currentIdentity.signature(); if (signature.isInlinedHtml() && signature.type() == KIdentityManagementCore::Signature::Inlined) { Q_EMIT enableHtml(); } KIdentityManagementCore::Signature::AddedText addedText = KIdentityManagementCore::Signature::AddNewLines; if (MessageComposer::MessageComposerSettings::self()->dashDashSignature()) { addedText |= KIdentityManagementCore::Signature::AddSeparator; } d->m_editor->insertSignature(signature, placement, addedText); if ((placement == KIdentityManagementCore::Signature::Start) || (placement == KIdentityManagementCore::Signature::End)) { Q_EMIT signatureAdded(); } } void SignatureController::applySignature(const KIdentityManagementCore::Signature &signature) { if (!d->m_editor) { return; } if (MessageComposer::MessageComposerSettings::self()->autoTextSignature() == QLatin1String("auto")) { KIdentityManagementCore::Signature::AddedText addedText = KIdentityManagementCore::Signature::AddNewLines; if (MessageComposer::MessageComposerSettings::self()->dashDashSignature()) { addedText |= KIdentityManagementCore::Signature::AddSeparator; } if (MessageComposer::MessageComposerSettings::self()->prependSignature()) { d->m_editor->insertSignature(signature, KIdentityManagementCore::Signature::Start, addedText); } else { d->m_editor->insertSignature(signature, KIdentityManagementCore::Signature::End, addedText); } } } #include "moc_signaturecontroller.cpp" diff --git a/server/editor/signaturecontroller.h b/server/editor/signaturecontroller.h index a14915a..ec44f0e 100644 --- a/server/editor/signaturecontroller.h +++ b/server/editor/signaturecontroller.h @@ -1,72 +1,72 @@ /* * SPDX-FileCopyrightText: 2010 Volker Krause * * Based on kmail/kmcomposewin.cpp * SPDX-FileCopyrightText: 2009 Constantin Berzan * * Based on KMail code by: * SPDX-FileCopyrightText: 1997 Markus Wuebben * * SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once -#include +#include "identity/signature.h" #include namespace KIdentityManagementWidgets { class IdentityCombo; } namespace MessageComposer { class RichTextComposerNg; class SignatureControllerPrivate; /** * @brief The SignatureController class * Controls signature (the footer thing, not the crypto thing) operations * happening on a KMEditor triggered by identity selection or menu actions. */ class SignatureController : public QObject { Q_OBJECT public: explicit SignatureController(QObject *parent = nullptr); ~SignatureController() override; void setEditor(MessageComposer::RichTextComposerNg *editor); void setIdentity(const KIdentityManagementCore::Identity &identity); /// Adds the given signature to the editor, taking user preferences into account. void applySignature(const KIdentityManagementCore::Signature &signature); public Q_SLOTS: /// Append signature to the end of the text in the editor. void appendSignature(); /// Prepend signature at the beginning of the text in the editor. void prependSignature(); /// Insert signature at the cursor position of the text in the editor. void insertSignatureAtCursor(); void cleanSpace(); Q_SIGNALS: /// A HTML signature is about to be inserted, so enable HTML support in the editor. void enableHtml(); void signatureAdded(); private: /// Helper to insert the signature of the current identity arbitrarily /// in the editor, connecting slot functions to KMeditor::insertSignature(). /// @param placement the position of the signature void insertSignatureHelper(KIdentityManagementCore::Signature::Placement placement); private: std::unique_ptr const d; }; } diff --git a/server/identity/identity.cpp b/server/identity/identity.cpp new file mode 100644 index 0000000..6b0ae09 --- /dev/null +++ b/server/identity/identity.cpp @@ -0,0 +1,814 @@ +/* + SPDX-FileCopyrightText: 2002-2004 Marc Mutz + SPDX-FileCopyrightText: 2007 Tom Albers + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "identity.h" + +#include "editor_debug.h" + +#include +#include + +#include +#include +#include + +using namespace KIdentityManagementCore; + +static Identity *identityNull = nullptr; + +Q_DECLARE_METATYPE(KIdentityManagementCore::Signature) + +Identity::Identity(const QString &id, const QString &fullName, const QString &emailAddr, const QString &organization, const QString &replyToAddr) +{ + qRegisterMetaType(); + setProperty(QLatin1StringView(s_uoid), 0); + setProperty(QLatin1StringView(s_identity), id); + setProperty(QLatin1StringView(s_name), fullName); + setProperty(QLatin1StringView(s_primaryEmail), emailAddr); + setProperty(QLatin1StringView(s_organization), organization); + setProperty(QLatin1StringView(s_replyto), replyToAddr); + // FIXME KF6: Sonnet::defaultLanguageName is gone + // setDictionary( Sonnet::defaultLanguageName() ); + setProperty(QLatin1StringView(s_disabledFcc), false); + setProperty(QLatin1StringView(s_defaultDomainName), QHostInfo::localHostName()); +} + +const Identity &Identity::null() +{ + if (!identityNull) { + identityNull = new Identity; + } + return *identityNull; +} + +bool Identity::isNull() const +{ + QHash::const_iterator i = mPropertiesMap.constBegin(); + + while (i != mPropertiesMap.constEnd()) { + const QString &key = i.key(); + // Take into account that the defaultDomainName for a null identity is not empty + if (key == QLatin1StringView(s_defaultDomainName)) { + ++i; + continue; + } + // Take into account that the dictionary for a null identity is not empty + if (key == QLatin1StringView(s_dict)) { + ++i; + continue; + } + // Take into account that disableFcc == false for a null identity + if (key == QLatin1StringView(s_disabledFcc) && i.value().toBool() == false) { + ++i; + continue; + } + // The uoid is 0 by default, so ignore this + if (!(key == QLatin1StringView(s_uoid) && i.value().toUInt() == 0)) { + if (!i.value().isNull() || (i.value().metaType().id() == QMetaType::QString && !i.value().toString().isEmpty())) { + return false; + } + } + ++i; + } + + return true; +} + +void Identity::readConfig(const KConfigGroup &config) +{ + // get all keys and convert them to our QHash. + QMap entries = config.entryMap(); + QMap::const_iterator i = entries.constBegin(); + QMap::const_iterator end = entries.constEnd(); + while (i != end) { + const QString &key = i.key(); + if (key == QLatin1StringView(s_emailAliases)) { + // HACK: Read s_emailAliases as a stringlist + mPropertiesMap.insert(key, config.readEntry(key, QStringList())); + } else { + mPropertiesMap.insert(key, config.readEntry(key)); + } + ++i; + } + // needs to update to v5.21.41 + // Check if we update to to encryption override mode + // before we had only auto_encrypt and auto_sign and no global setting + if (!mPropertiesMap.contains(QLatin1StringView(s_encryptionOverride)) && !mPropertiesMap.contains(QLatin1StringView(s_warnnotencrypt)) + && !mPropertiesMap.contains(QLatin1StringView(s_warnnotsign))) { + setEncryptionOverride(true); + } + mSignature.readConfig(config); +} + +void Identity::writeConfig(KConfigGroup &config) const +{ + QHash::const_iterator i = mPropertiesMap.constBegin(); + QHash::const_iterator end = mPropertiesMap.constEnd(); + while (i != end) { + config.writeEntry(i.key(), i.value()); + qCDebug(EDITOR_LOG) << "Store:" << i.key() << ":" << i.value(); + ++i; + } + if (!mPropertiesMap.contains(QLatin1StringView(s_encryptionOverride))) { + config.writeEntry(QLatin1StringView(s_encryptionOverride), false); + qCDebug(EDITOR_LOG) << "Add" << s_encryptionOverride << ":" << false; + } + mSignature.writeConfig(config); +} + +bool Identity::mailingAllowed() const +{ + return !property(QLatin1StringView(s_primaryEmail)).toString().isEmpty(); +} + +QString Identity::mimeDataType() +{ + return QStringLiteral("application/x-kmail-identity-drag"); +} + +bool Identity::canDecode(const QMimeData *md) +{ + if (md) { + return md->hasFormat(mimeDataType()); + } else { + return false; + } +} + +void Identity::populateMimeData(QMimeData *md) const +{ + QByteArray a; + { + QDataStream s(&a, QIODevice::WriteOnly); + s << *this; + } + md->setData(mimeDataType(), a); +} + +Identity Identity::fromMimeData(const QMimeData *md) +{ + Identity i; + if (canDecode(md)) { + QByteArray ba = md->data(mimeDataType()); + QDataStream s(&ba, QIODevice::ReadOnly); + s >> i; + } + return i; +} + +// ------------------ Operators --------------------------// + +QDataStream &KIdentityManagementCore::operator<<(QDataStream &stream, const KIdentityManagementCore::Identity &i) +{ + return stream << static_cast(i.uoid()) << i.mPropertiesMap[QLatin1StringView(s_identity)] << i.mPropertiesMap[QLatin1StringView(s_name)] + << i.mPropertiesMap[QLatin1StringView(s_organization)] << i.mPropertiesMap[QLatin1StringView(s_pgps)] + << i.mPropertiesMap[QLatin1StringView(s_pgpe)] << i.mPropertiesMap[QLatin1StringView(s_smimes)] + << i.mPropertiesMap[QLatin1StringView(s_smimee)] << i.mPropertiesMap[QLatin1StringView(s_primaryEmail)] + << i.mPropertiesMap[QLatin1StringView(s_emailAliases)] << i.mPropertiesMap[QLatin1StringView(s_replyto)] + << i.mPropertiesMap[QLatin1StringView(s_bcc)] << i.mPropertiesMap[QLatin1StringView(s_vcard)] + << i.mPropertiesMap[QLatin1StringView(s_transport)] << i.mPropertiesMap[QLatin1StringView(s_fcc)] + << i.mPropertiesMap[QLatin1StringView(s_drafts)] << i.mPropertiesMap[QLatin1StringView(s_templates)] << i.mSignature + << i.mPropertiesMap[QLatin1StringView(s_dict)] << i.mPropertiesMap[QLatin1StringView(s_xface)] + << i.mPropertiesMap[QLatin1StringView(s_xfaceenabled)] << i.mPropertiesMap[QLatin1StringView(s_face)] + << i.mPropertiesMap[QLatin1StringView(s_faceenabled)] << i.mPropertiesMap[QLatin1StringView(s_prefcrypt)] + << i.mPropertiesMap[QLatin1StringView(s_cc)] << i.mPropertiesMap[QLatin1StringView(s_attachVcard)] + << i.mPropertiesMap[QLatin1StringView(s_autocorrectionLanguage)] << i.mPropertiesMap[QLatin1StringView(s_disabledFcc)] + << i.mPropertiesMap[QLatin1StringView(s_defaultDomainName)] << i.mPropertiesMap[QLatin1StringView(s_autocryptEnabled)] + << i.mPropertiesMap[QLatin1StringView(s_autocryptPrefer)] << i.mPropertiesMap[QLatin1StringView(s_encryptionOverride)] + << i.mPropertiesMap[QLatin1StringView(s_pgpautosign)] << i.mPropertiesMap[QLatin1StringView(s_pgpautoencrypt)] + << i.mPropertiesMap[QLatin1StringView(s_warnnotencrypt)] << i.mPropertiesMap[QLatin1StringView(s_warnnotsign)]; +} + +QDataStream &KIdentityManagementCore::operator>>(QDataStream &stream, KIdentityManagementCore::Identity &i) +{ + quint32 uoid; + stream >> uoid >> i.mPropertiesMap[QLatin1StringView(s_identity)] >> i.mPropertiesMap[QLatin1StringView(s_name)] + >> i.mPropertiesMap[QLatin1StringView(s_organization)] >> i.mPropertiesMap[QLatin1StringView(s_pgps)] >> i.mPropertiesMap[QLatin1StringView(s_pgpe)] + >> i.mPropertiesMap[QLatin1StringView(s_smimes)] >> i.mPropertiesMap[QLatin1StringView(s_smimee)] >> i.mPropertiesMap[QLatin1StringView(s_primaryEmail)] + >> i.mPropertiesMap[QLatin1StringView(s_emailAliases)] >> i.mPropertiesMap[QLatin1StringView(s_replyto)] >> i.mPropertiesMap[QLatin1StringView(s_bcc)] + >> i.mPropertiesMap[QLatin1StringView(s_vcard)] >> i.mPropertiesMap[QLatin1StringView(s_transport)] >> i.mPropertiesMap[QLatin1StringView(s_fcc)] + >> i.mPropertiesMap[QLatin1StringView(s_drafts)] >> i.mPropertiesMap[QLatin1StringView(s_templates)] >> i.mSignature + >> i.mPropertiesMap[QLatin1StringView(s_dict)] >> i.mPropertiesMap[QLatin1StringView(s_xface)] >> i.mPropertiesMap[QLatin1StringView(s_xfaceenabled)] + >> i.mPropertiesMap[QLatin1StringView(s_face)] >> i.mPropertiesMap[QLatin1StringView(s_faceenabled)] >> i.mPropertiesMap[QLatin1StringView(s_prefcrypt)] + >> i.mPropertiesMap[QLatin1StringView(s_cc)] >> i.mPropertiesMap[QLatin1StringView(s_attachVcard)] + >> i.mPropertiesMap[QLatin1StringView(s_autocorrectionLanguage)] >> i.mPropertiesMap[QLatin1StringView(s_disabledFcc)] + >> i.mPropertiesMap[QLatin1StringView(s_defaultDomainName)] >> i.mPropertiesMap[QLatin1StringView(s_autocryptEnabled)] + >> i.mPropertiesMap[QLatin1StringView(s_autocryptPrefer)] >> i.mPropertiesMap[QLatin1StringView(s_encryptionOverride)] + >> i.mPropertiesMap[QLatin1StringView(s_pgpautosign)] >> i.mPropertiesMap[QLatin1StringView(s_pgpautoencrypt)] + >> i.mPropertiesMap[QLatin1StringView(s_warnnotencrypt)] >> i.mPropertiesMap[QLatin1StringView(s_warnnotsign)]; + + i.setProperty(QLatin1StringView(s_uoid), uoid); + return stream; +} + +bool Identity::operator<(const Identity &other) const +{ + if (isDefault()) { + return true; + } + if (other.isDefault()) { + return false; + } + return identityName() < other.identityName(); +} + +bool Identity::operator>(const Identity &other) const +{ + if (isDefault()) { + return false; + } + if (other.isDefault()) { + return true; + } + return identityName() > other.identityName(); +} + +bool Identity::operator<=(const Identity &other) const +{ + return !operator>(other); +} + +bool Identity::operator>=(const Identity &other) const +{ + return !operator<(other); +} + +bool Identity::operator==(const Identity &other) const +{ + // The deserializer fills in the QHash will lots of invalid variants, which + // is OK, but the CTOR doesn't fill the hash with the missing fields, so + // regular mPropertiesMap == other.mPropertiesMap comparison will fail. + // This algo considers both maps equal even if one map does not contain the + // key and the other one contains the key but with an invalid value + for (const auto &pair : {qMakePair(mPropertiesMap, other.mPropertiesMap), qMakePair(other.mPropertiesMap, mPropertiesMap)}) { + const auto lhs = pair.first; + const auto rhs = pair.second; + for (auto lhsIt = lhs.constBegin(), lhsEnd = lhs.constEnd(); lhsIt != lhsEnd; ++lhsIt) { + const auto rhsIt = rhs.constFind(lhsIt.key()); + // Does the other map contain the key? + if (rhsIt == rhs.constEnd()) { + // It does not, so check if our value is invalid, if yes, consider it + // equal to not present and continue + if (lhsIt->isValid()) { + return false; + } + } else if (lhsIt.value() != rhsIt.value()) { + // Both maps have the key, but different value -> different maps + return false; + } + } + } + + return mSignature == other.mSignature; +} + +bool Identity::operator!=(const Identity &other) const +{ + return !operator==(other); +} + +// --------------------- Getters -----------------------------// + +QVariant Identity::property(const QString &key) const +{ + if (key == QLatin1StringView(s_signature)) { + return QVariant::fromValue(mSignature); + } else { + return mPropertiesMap.value(key); + } +} + +QString Identity::fullEmailAddr() const +{ + const QString name = mPropertiesMap.value(QLatin1StringView(s_name)).toString(); + const QString mail = mPropertiesMap.value(QLatin1StringView(s_primaryEmail)).toString(); + + if (name.isEmpty()) { + return mail; + } + + const QString specials(QStringLiteral("()<>@,.;:[]")); + + QString result; + + // add DQUOTE's if necessary: + bool needsQuotes = false; + const int nameLength(name.length()); + for (int i = 0; i < nameLength; i++) { + if (specials.contains(name[i])) { + needsQuotes = true; + } else if (name[i] == QLatin1Char('\\') || name[i] == QLatin1Char('"')) { + needsQuotes = true; + result += QLatin1Char('\\'); + } + result += name[i]; + } + + if (needsQuotes) { + result.insert(0, QLatin1Char('"')); + result += QLatin1Char('"'); + } + + result += QLatin1StringView(" <") + mail + QLatin1Char('>'); + + return result; +} + +QString Identity::identityName() const +{ + return property(QLatin1StringView(s_identity)).toString(); +} + +QString Identity::signatureText(bool *ok) const +{ + return mSignature.withSeparator(ok); +} + +bool Identity::signatureIsInlinedHtml() const +{ + return mSignature.isInlinedHtml(); +} + +bool Identity::isDefault() const +{ + return mIsDefault; +} + +uint Identity::uoid() const +{ + return property(QLatin1StringView(s_uoid)).toInt(); +} + +QString Identity::fullName() const +{ + return property(QLatin1StringView(s_name)).toString(); +} + +QString Identity::organization() const +{ + return property(QLatin1StringView(s_organization)).toString(); +} + +QByteArray Identity::pgpEncryptionKey() const +{ + return property(QLatin1StringView(s_pgpe)).toByteArray(); +} + +QByteArray Identity::pgpSigningKey() const +{ + return property(QLatin1StringView(s_pgps)).toByteArray(); +} + +QByteArray Identity::smimeEncryptionKey() const +{ + return property(QLatin1StringView(s_smimee)).toByteArray(); +} + +QByteArray Identity::smimeSigningKey() const +{ + return property(QLatin1StringView(s_smimes)).toByteArray(); +} + +QString Identity::preferredCryptoMessageFormat() const +{ + return property(QLatin1StringView(s_prefcrypt)).toString(); +} + +QString Identity::primaryEmailAddress() const +{ + return property(QLatin1StringView(s_primaryEmail)).toString(); +} + +const QStringList Identity::emailAliases() const +{ + return property(QLatin1StringView(s_emailAliases)).toStringList(); +} + +QString Identity::vCardFile() const +{ + return property(QLatin1StringView(s_vcard)).toString(); +} + +bool Identity::attachVcard() const +{ + return property(QLatin1StringView(s_attachVcard)).toBool(); +} + +QString Identity::replyToAddr() const +{ + return property(QLatin1StringView(s_replyto)).toString(); +} + +QString Identity::bcc() const +{ + return property(QLatin1StringView(s_bcc)).toString(); +} + +QString Identity::cc() const +{ + return property(QLatin1StringView(s_cc)).toString(); +} + +Signature &Identity::signature() +{ + return mSignature; +} + +bool Identity::isXFaceEnabled() const +{ + return property(QLatin1StringView(s_xfaceenabled)).toBool(); +} + +QString Identity::xface() const +{ + return property(QLatin1StringView(s_xface)).toString(); +} + +bool Identity::isFaceEnabled() const +{ + return property(QLatin1StringView(s_faceenabled)).toBool(); +} + +QString Identity::face() const +{ + return property(QLatin1StringView(s_face)).toString(); +} + +QString Identity::dictionary() const +{ + return property(QLatin1StringView(s_dict)).toString(); +} + +QString Identity::templates() const +{ + const QString str = property(QLatin1StringView(s_templates)).toString(); + return verifyAkonadiId(str); +} + +QString Identity::drafts() const +{ + const QString str = property(QLatin1StringView(s_drafts)).toString(); + return verifyAkonadiId(str); +} + +QString Identity::fcc() const +{ + const QString str = property(QLatin1StringView(s_fcc)).toString(); + return verifyAkonadiId(str); +} + +QString Identity::transport() const +{ + return property(QLatin1StringView(s_transport)).toString(); +} + +bool Identity::signatureIsInline() const +{ + return mSignature.type() == Signature::Inlined; +} + +QString Identity::signatureInlineText() const +{ + return mSignature.text(); +} + +QString Identity::autocorrectionLanguage() const +{ + return property(QLatin1StringView(s_autocorrectionLanguage)).toString(); +} + +// --------------------- Setters -----------------------------// + +void Identity::setProperty(const QString &key, const QVariant &value) +{ + if (key == QLatin1StringView(s_signature)) { + mSignature = value.value(); + } else { + if (value.isNull() || (value.metaType().id() == QMetaType::QString && value.toString().isEmpty())) { + mPropertiesMap.remove(key); + } else { + mPropertiesMap.insert(key, value); + } + } +} + +void Identity::setUoid(uint aUoid) +{ + setProperty(QLatin1StringView(s_uoid), aUoid); +} + +void Identity::setIdentityName(const QString &name) +{ + setProperty(QLatin1StringView(s_identity), name); +} + +void Identity::setFullName(const QString &str) +{ + setProperty(QLatin1StringView(s_name), str); +} + +void Identity::setOrganization(const QString &str) +{ + setProperty(QLatin1StringView(s_organization), str); +} + +void Identity::setPGPSigningKey(const QByteArray &str) +{ + setProperty(QLatin1StringView(s_pgps), QLatin1StringView(str)); +} + +void Identity::setPGPEncryptionKey(const QByteArray &str) +{ + setProperty(QLatin1StringView(s_pgpe), QLatin1StringView(str)); +} + +void Identity::setSMIMESigningKey(const QByteArray &str) +{ + setProperty(QLatin1StringView(s_smimes), QLatin1StringView(str)); +} + +void Identity::setSMIMEEncryptionKey(const QByteArray &str) +{ + setProperty(QLatin1StringView(s_smimee), QLatin1StringView(str)); +} + +void Identity::setPrimaryEmailAddress(const QString &email) +{ + setProperty(QLatin1StringView(s_primaryEmail), email); +} + +void Identity::setEmailAliases(const QStringList &aliases) +{ + setProperty(QLatin1StringView(s_emailAliases), aliases); +} + +void Identity::setVCardFile(const QString &str) +{ + setProperty(QLatin1StringView(s_vcard), str); +} + +void Identity::setAttachVcard(bool attachment) +{ + setProperty(QLatin1StringView(s_attachVcard), attachment); +} + +void Identity::setReplyToAddr(const QString &str) +{ + setProperty(QLatin1StringView(s_replyto), str); +} + +void Identity::setSignatureInlineText(const QString &str) +{ + mSignature.setText(str); +} + +void Identity::setTransport(const QString &str) +{ + setProperty(QLatin1StringView(s_transport), str); +} + +void Identity::setFcc(const QString &str) +{ + setProperty(QLatin1StringView(s_fcc), str); +} + +void Identity::setDrafts(const QString &str) +{ + setProperty(QLatin1StringView(s_drafts), str); +} + +void Identity::setTemplates(const QString &str) +{ + setProperty(QLatin1StringView(s_templates), str); +} + +void Identity::setDictionary(const QString &str) +{ + setProperty(QLatin1StringView(s_dict), str); +} + +void Identity::setBcc(const QString &str) +{ + setProperty(QLatin1StringView(s_bcc), str); +} + +void Identity::setCc(const QString &str) +{ + setProperty(QLatin1StringView(s_cc), str); +} + +void Identity::setIsDefault(bool flag) +{ + mIsDefault = flag; +} + +void Identity::setPreferredCryptoMessageFormat(const QString &str) +{ + setProperty(QLatin1StringView(s_prefcrypt), str); +} + +void Identity::setXFace(const QString &str) +{ + QString strNew = str; + strNew.remove(QLatin1Char(' ')); + strNew.remove(QLatin1Char('\n')); + strNew.remove(QLatin1Char('\r')); + setProperty(QLatin1StringView(s_xface), strNew); +} + +void Identity::setXFaceEnabled(bool on) +{ + setProperty(QLatin1StringView(s_xfaceenabled), on); +} + +void Identity::setFace(const QString &str) +{ + QString strNew = str; + strNew.remove(QLatin1Char(' ')); + strNew.remove(QLatin1Char('\n')); + strNew.remove(QLatin1Char('\r')); + setProperty(QLatin1StringView(s_face), strNew); +} + +void Identity::setFaceEnabled(bool on) +{ + setProperty(QLatin1StringView(s_faceenabled), on); +} + +void Identity::setSignature(const Signature &sig) +{ + mSignature = sig; +} + +bool Identity::matchesEmailAddress(const QString &addr) const +{ + const QString addrSpec = KEmailAddress::extractEmailAddress(addr).toLower(); + if (addrSpec == primaryEmailAddress().toLower()) { + return true; + } + + const QStringList lst = emailAliases(); + for (const QString &alias : lst) { + if (alias.toLower() == addrSpec) { + return true; + } + } + + return false; +} + +void Identity::setAutocorrectionLanguage(const QString &language) +{ + setProperty(QLatin1StringView(s_autocorrectionLanguage), language); +} + +bool Identity::disabledFcc() const +{ + const QVariant var = property(QLatin1StringView(s_disabledFcc)); + if (var.isNull()) { + return false; + } else { + return var.toBool(); + } +} + +void Identity::setDisabledFcc(bool disable) +{ + setProperty(QLatin1StringView(s_disabledFcc), disable); +} + +bool Identity::pgpAutoSign() const +{ + const QVariant var = property(QLatin1StringView(s_pgpautosign)); + if (var.isNull()) { + return false; + } else { + return var.toBool(); + } +} + +void Identity::setPgpAutoSign(bool autoSign) +{ + setProperty(QLatin1StringView(s_pgpautosign), autoSign); +} + +bool Identity::pgpAutoEncrypt() const +{ + const QVariant var = property(QLatin1StringView(s_pgpautoencrypt)); + if (var.isNull()) { + return false; + } else { + return var.toBool(); + } +} + +void Identity::setPgpAutoEncrypt(bool autoEncrypt) +{ + setProperty(QLatin1StringView(s_pgpautoencrypt), autoEncrypt); +} + +bool Identity::autocryptEnabled() const +{ + const auto var = property(QLatin1StringView(s_autocryptEnabled)); + if (var.isNull()) { + return false; + } else { + return var.toBool(); + } +} + +void Identity::setAutocryptEnabled(const bool on) +{ + setProperty(QLatin1StringView(s_autocryptEnabled), on); +} + +bool Identity::autocryptPrefer() const +{ + const auto var = property(QLatin1StringView(s_autocryptPrefer)); + if (var.isNull()) { + return false; + } else { + return var.toBool(); + } +} + +void Identity::setAutocryptPrefer(const bool on) +{ + setProperty(QLatin1StringView(s_autocryptPrefer), on); +} + +bool Identity::encryptionOverride() const +{ + const auto var = property(QLatin1StringView(s_encryptionOverride)); + if (var.isNull()) { + return false; + } else { + return var.toBool(); + } +} + +void Identity::setEncryptionOverride(const bool on) +{ + setProperty(QLatin1StringView(s_encryptionOverride), on); +} + +bool Identity::warnNotEncrypt() const +{ + const auto var = property(QLatin1StringView(s_warnnotencrypt)); + if (var.isNull()) { + return false; + } else { + return var.toBool(); + } +} + +void Identity::setWarnNotEncrypt(const bool on) +{ + setProperty(QLatin1StringView(s_warnnotencrypt), on); +} + +bool Identity::warnNotSign() const +{ + const auto var = property(QLatin1StringView(s_warnnotsign)); + if (var.isNull()) { + return false; + } else { + return var.toBool(); + } +} + +void Identity::setWarnNotSign(const bool on) +{ + setProperty(QLatin1StringView(s_warnnotsign), on); +} + +QString Identity::defaultDomainName() const +{ + return property(QLatin1StringView(s_defaultDomainName)).toString(); +} + +void Identity::setDefaultDomainName(const QString &domainName) +{ + setProperty(QLatin1StringView(s_defaultDomainName), domainName); +} + +QString Identity::verifyAkonadiId(const QString &str) const +{ + if (str.isEmpty()) { + return str; + } + bool ok = false; + const qlonglong val = str.toLongLong(&ok); + Q_UNUSED(val) + if (ok) { + return str; + } else { + return {}; + } +} + +#include "moc_identity.cpp" diff --git a/server/identity/identity.h b/server/identity/identity.h new file mode 100644 index 0000000..8a976e8 --- /dev/null +++ b/server/identity/identity.h @@ -0,0 +1,455 @@ +/* + SPDX-FileCopyrightText: 2002-2004 Marc Mutz + SPDX-FileCopyrightText: 2007 Tom Albers + Author: Stefan Taferner + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "signature.h" + +#include +#include +#include +#include +#include + +namespace KIdentityManagementCore +{ +class Identity; +} +class KConfigGroup; +class QDataStream; +class QMimeData; + +namespace KIdentityManagementCore +{ +static const char s_uoid[] = "uoid"; +static const char s_identity[] = "Identity"; +static const char s_name[] = "Name"; +static const char s_organization[] = "Organization"; +static const char s_pgps[] = "PGP Signing Key"; +static const char s_pgpe[] = "PGP Encryption Key"; +static const char s_smimes[] = "SMIME Signing Key"; +static const char s_smimee[] = "SMIME Encryption Key"; +static const char s_prefcrypt[] = "Preferred Crypto Message Format"; +static const char s_primaryEmail[] = "Email Address"; +static const char s_replyto[] = "Reply-To Address"; +static const char s_bcc[] = "Bcc"; +static const char s_cc[] = "Cc"; +static const char s_vcard[] = "VCardFile"; +static const char s_transport[] = "Transport"; +static const char s_fcc[] = "Fcc"; +static const char s_drafts[] = "Drafts"; +static const char s_templates[] = "Templates"; +static const char s_dict[] = "Dictionary"; +static const char s_xface[] = "X-Face"; +static const char s_xfaceenabled[] = "X-FaceEnabled"; +static const char s_face[] = "Face"; +static const char s_faceenabled[] = "FaceEnabled"; +static const char s_signature[] = "Signature"; +static const char s_emailAliases[] = "Email Aliases"; +static const char s_attachVcard[] = "Attach Vcard"; +static const char s_autocorrectionLanguage[] = "Autocorrection Language"; +static const char s_disabledFcc[] = "Disable Fcc"; +static const char s_encryptionOverride[] = "Override Encryption Defaults"; +static const char s_pgpautosign[] = "Pgp Auto Sign"; +static const char s_pgpautoencrypt[] = "Pgp Auto Encrypt"; +static const char s_warnnotsign[] = "Warn not Sign"; +static const char s_warnnotencrypt[] = "Warn not Encrypt"; +static const char s_defaultDomainName[] = "Default Domain"; +static const char s_autocryptEnabled[] = "Autocrypt"; +static const char s_autocryptPrefer[] = "Autocrypt Prefer"; + +QDataStream &operator<<(QDataStream &stream, const KIdentityManagementCore::Identity &ident); +QDataStream &operator>>(QDataStream &stream, KIdentityManagementCore::Identity &ident); + +/** User identity information */ +class Identity +{ + Q_GADGET + + Q_PROPERTY(bool mailingAllowed READ mailingAllowed) + Q_PROPERTY(QString identityName READ identityName WRITE setIdentityName) + Q_PROPERTY(QString fullName READ fullName WRITE setFullName) + Q_PROPERTY(QString organization READ organization WRITE setOrganization) + Q_PROPERTY(QByteArray pgpEncryptionKey READ pgpEncryptionKey WRITE setPGPEncryptionKey) + Q_PROPERTY(QByteArray pgpSigningKey READ pgpSigningKey WRITE setPGPSigningKey) + Q_PROPERTY(QByteArray smimeEncryptionKey READ smimeEncryptionKey WRITE setSMIMEEncryptionKey) + Q_PROPERTY(QByteArray smimeSigningKey READ smimeSigningKey WRITE setSMIMESigningKey) + Q_PROPERTY(QString preferredCryptoMessageFormat READ preferredCryptoMessageFormat WRITE setPreferredCryptoMessageFormat) + Q_PROPERTY(QString primaryEmailAddress READ primaryEmailAddress WRITE setPrimaryEmailAddress) + Q_PROPERTY(QStringList emailAliases READ emailAliases WRITE setEmailAliases) + Q_PROPERTY(QString vCardFile READ vCardFile WRITE setVCardFile) + Q_PROPERTY(QString fullEmailAddr READ fullEmailAddr) + Q_PROPERTY(QString replyToAddr READ replyToAddr WRITE setReplyToAddr) + Q_PROPERTY(QString bcc READ bcc WRITE setBcc) + Q_PROPERTY(QString cc READ cc WRITE setCc) + Q_PROPERTY(bool attachVcard READ attachVcard WRITE setAttachVcard) + Q_PROPERTY(QString autocorrectionLanguage READ autocorrectionLanguage WRITE setAutocorrectionLanguage) + Q_PROPERTY(bool disabledFcc READ disabledFcc WRITE setDisabledFcc) + Q_PROPERTY(bool pgpAutoSign READ pgpAutoSign WRITE setPgpAutoSign) + Q_PROPERTY(bool pgpAutoEncrypt READ pgpAutoEncrypt WRITE setPgpAutoEncrypt) + Q_PROPERTY(bool autocryptEnabled READ autocryptEnabled WRITE setAutocryptEnabled) + Q_PROPERTY(bool autocryptPrefer READ autocryptPrefer WRITE setAutocryptPrefer) + Q_PROPERTY(bool encryptionOverride READ encryptionOverride WRITE setEncryptionOverride) + Q_PROPERTY(bool warnNotSign READ warnNotSign WRITE setWarnNotSign) + Q_PROPERTY(bool warnNotEncrypt READ warnNotEncrypt WRITE setWarnNotEncrypt) + Q_PROPERTY(QString defaultDomainName READ defaultDomainName WRITE setDefaultDomainName) + Q_PROPERTY(Signature signature READ signature WRITE setSignature) + Q_PROPERTY(QString signatureText READ signatureText) + Q_PROPERTY(bool signatureIsInlinedHtml READ signatureIsInlinedHtml) + Q_PROPERTY(QString transport READ transport WRITE setTransport) + Q_PROPERTY(QString fcc READ fcc WRITE setFcc) + Q_PROPERTY(QString drafts READ drafts WRITE setDrafts) + Q_PROPERTY(QString templates READ templates WRITE setTemplates) + Q_PROPERTY(QString dictionary READ dictionary WRITE setDictionary) + Q_PROPERTY(QString xface READ xface WRITE setXFace) + Q_PROPERTY(bool isXFaceEnabled READ isXFaceEnabled WRITE setXFaceEnabled) + Q_PROPERTY(QString face READ face WRITE setFace) + Q_PROPERTY(bool isFaceEnabled READ isFaceEnabled WRITE setFaceEnabled) + Q_PROPERTY(uint uoid READ uoid CONSTANT) + Q_PROPERTY(bool isNull READ isNull) + + // only the identity manager should be able to construct and + // destruct us, but then we get into problems with using + // QValueList and especially qHeapSort(). + friend class IdentityManager; + + friend QDataStream &operator<<(QDataStream &stream, const KIdentityManagementCore::Identity &ident); + friend QDataStream &operator>>(QDataStream &stream, KIdentityManagementCore::Identity &ident); + +public: + using List = QList; + using Id = uint; + + /** Constructor */ + explicit Identity(const QString &id = QString(), + const QString &realName = QString(), + const QString &emailAddr = QString(), + const QString &organization = QString(), + const QString &replyToAddress = QString()); + + /** used for comparison */ + bool operator==(const Identity &other) const; + + /** used for comparison */ + bool operator!=(const Identity &other) const; + + /** used for sorting */ + bool operator<(const Identity &other) const; + + /** used for sorting */ + bool operator>(const Identity &other) const; + + /** used for sorting */ + bool operator<=(const Identity &other) const; + + /** used for sorting */ + bool operator>=(const Identity &other) const; + + /** Tests if there are enough values set to allow mailing */ + [[nodiscard]] bool mailingAllowed() const; + + /** Identity/nickname for this collection */ + [[nodiscard]] QString identityName() const; + + /** Identity/nickname for this collection */ + void setIdentityName(const QString &name); + + /** @return whether this identity is the default identity */ + [[nodiscard]] bool isDefault() const; + + /** Unique Object Identifier for this identity */ + [[nodiscard]] uint uoid() const; + + /** Full name of the user */ + [[nodiscard]] QString fullName() const; + void setFullName(const QString &); + + /** The user's organization (optional) */ + [[nodiscard]] QString organization() const; + void setOrganization(const QString &); + + /** The user's OpenPGP encryption key */ + [[nodiscard]] QByteArray pgpEncryptionKey() const; + void setPGPEncryptionKey(const QByteArray &key); + + /** The user's OpenPGP signing key */ + [[nodiscard]] QByteArray pgpSigningKey() const; + void setPGPSigningKey(const QByteArray &key); + + /** The user's S/MIME encryption key */ + [[nodiscard]] QByteArray smimeEncryptionKey() const; + void setSMIMEEncryptionKey(const QByteArray &key); + + /** The user's S/MIME signing key */ + [[nodiscard]] QByteArray smimeSigningKey() const; + void setSMIMESigningKey(const QByteArray &key); + + [[nodiscard]] QString preferredCryptoMessageFormat() const; + void setPreferredCryptoMessageFormat(const QString &); + + /** + * The primary email address (without the user name - only name\@host). + * + * This email address is used for all outgoing mail. + * + * @since 4.6 + */ + [[nodiscard]] QString primaryEmailAddress() const; + void setPrimaryEmailAddress(const QString &email); + + /** + * The email address aliases + * + * @since 4.6 + */ + [[nodiscard]] const QStringList emailAliases() const; + void setEmailAliases(const QStringList &aliases); + + /** + * @param addr the email address to check + * @return true if this identity contains the email address @p addr, either as primary address + * or as alias + * + * @since 4.6 + */ + [[nodiscard]] bool matchesEmailAddress(const QString &addr) const; + + /** vCard to attach to outgoing emails */ + [[nodiscard]] QString vCardFile() const; + void setVCardFile(const QString &); + + /** + * The email address in the format "username " suitable + * for the "From:" field of email messages. + */ + [[nodiscard]] QString fullEmailAddr() const; + + /** @return The email address for the ReplyTo: field */ + [[nodiscard]] QString replyToAddr() const; + void setReplyToAddr(const QString &); + + /** @return The email addresses for the BCC: field */ + [[nodiscard]] QString bcc() const; + void setBcc(const QString &); + + /** + * @return The email addresses for the CC: field + * @since 4.9 + */ + [[nodiscard]] QString cc() const; + void setCc(const QString &); + + /** + * @return true if the Vcard of this identity should be attached to outgoing mail. + * @since 4.10 + */ + [[nodiscard]] bool attachVcard() const; + void setAttachVcard(bool attach); + + /** + * @return The default language for spell checking of this identity. + * @since 4.10 + */ + [[nodiscard]] QString autocorrectionLanguage() const; + void setAutocorrectionLanguage(const QString &language); + + /** + * @return true if Fcc is disabled for this identity. + * @since 4.11 + */ + [[nodiscard]] bool disabledFcc() const; + void setDisabledFcc(bool); + + /** + * @return true if we should sign message sent by this identity by default. + * @since 4.12 + */ + [[nodiscard]] bool pgpAutoSign() const; + void setPgpAutoSign(bool); + + /** + * @return true if we should encrypt message sent by this identity by default. + * @since 5.4 + */ + [[nodiscard]] bool pgpAutoEncrypt() const; + void setPgpAutoEncrypt(bool); + + /** + * @return true if Autocrypt is enabled for this identity. + * @since 5.17 + */ + [[nodiscard]] bool autocryptEnabled() const; + void setAutocryptEnabled(const bool); + + /** + * @return true if Autocrypt is preferred for this identity. + * @since 5.22 + */ + [[nodiscard]] bool autocryptPrefer() const; + void setAutocryptPrefer(const bool); + + /** + * @return true if the warnNotSign and warnNotEncrypt identity configuration should + * overwrite the global app-wide configuration. + * @since 5.22 + */ + [[nodiscard]] bool encryptionOverride() const; + void setEncryptionOverride(const bool); + + /** + * @return true if we should warn if parts of the message this identity is about to send are not signed. + * @since 5.22 + */ + [[nodiscard]] bool warnNotSign() const; + void setWarnNotSign(const bool); + + /** + * @return true if we should warn if parts of the message this identity is about to send are not encrypted. + * @since 5.22 + */ + [[nodiscard]] bool warnNotEncrypt() const; + void setWarnNotEncrypt(const bool); + + /** + * @return The default domain name + * @since 4.14 + */ + [[nodiscard]] QString defaultDomainName() const; + void setDefaultDomainName(const QString &domainName); + + /** + * @return The signature of the identity. + * + * @warning This method is not const. + */ + [[nodiscard]] Signature &signature(); + void setSignature(const Signature &sig); + + /** + * @return the signature with '-- \n' prepended to it if it is not + * present already. + * No newline in front of or after the signature is added. + * @param ok if a valid bool pointer, it is set to @c true or @c false depending + * on whether the signature could successfully be obtained. + */ + [[nodiscard]] QString signatureText(bool *ok = nullptr) const; + + /** + * @return true if the inlined signature is html formatted + * @since 4.1 + */ + [[nodiscard]] bool signatureIsInlinedHtml() const; + + /** The transport that is set for this identity. Used to link a + transport with an identity. */ + [[nodiscard]] QString transport() const; + void setTransport(const QString &); + + /** The folder where sent messages from this identity will be + stored by default. */ + [[nodiscard]] QString fcc() const; + void setFcc(const QString &); + + /** The folder where draft messages from this identity will be + stored by default. + */ + [[nodiscard]] QString drafts() const; + void setDrafts(const QString &); + + /** The folder where template messages from this identity will be + stored by default. + */ + [[nodiscard]] QString templates() const; + void setTemplates(const QString &); + + /** + * Dictionary which should be used for spell checking + * + * Note that this is the localized language name (e.g. "British English"), + * _not_ the language code or dictionary name! + */ + [[nodiscard]] QString dictionary() const; + void setDictionary(const QString &); + + /** a X-Face header for this identity */ + [[nodiscard]] QString xface() const; + void setXFace(const QString &); + [[nodiscard]] bool isXFaceEnabled() const; + void setXFaceEnabled(bool); + + /** a Face header for this identity */ + [[nodiscard]] QString face() const; + void setFace(const QString &); + [[nodiscard]] bool isFaceEnabled() const; + void setFaceEnabled(bool); + + /** Get random properties + * @param key the key of the property to get + */ + [[nodiscard]] QVariant property(const QString &key) const; + /** Set random properties, when @p value is empty (for QStrings) or null, + the property is deleted. */ + void setProperty(const QString &key, const QVariant &value); + + static const Identity &null(); + /** Returns true when the identity contains no values, all null values or + only empty values */ + [[nodiscard]] bool isNull() const; + + [[nodiscard]] static QString mimeDataType(); + [[nodiscard]] static bool canDecode(const QMimeData *); + void populateMimeData(QMimeData *) const; + static Identity fromMimeData(const QMimeData *); + + /** Read configuration from config. Group must be preset (or use + KConfigGroup). Called from IdentityManager. */ + void readConfig(const KConfigGroup &); + + /** Write configuration to config. Group must be preset (or use + KConfigGroup). Called from IdentityManager. */ + void writeConfig(KConfigGroup &) const; + + /** Set whether this identity is the default identity. Since this + affects all other identities, too (most notably, the old default + identity), only the IdentityManager can change this. + You should use +
+        kmkernel->identityManager()->setAsDefault( name_of_default )
+        
+ instead. */ + void setIsDefault(bool flag); + + /** + * Set the uiod + * @param aUoid the uoid to set + */ + void setUoid(uint aUoid); + +protected: + /** during migration when it failed it can be a string => not a qlonglong akonadi::id => fix it*/ + [[nodiscard]] QString verifyAkonadiId(const QString &str) const; + + /** @return true if the signature was specified directly */ + [[nodiscard]] bool signatureIsInline() const; + + /** inline signature */ + [[nodiscard]] QString signatureInlineText() const; + void setSignatureInlineText(const QString &); + + /** Inline or signature from a file */ + [[nodiscard]] bool useSignatureFile() const; + + Signature mSignature; + bool mIsDefault = false; + QHash mPropertiesMap; +}; +} + +#ifndef UNITY_CMAKE_SUPPORT +Q_DECLARE_METATYPE(KIdentityManagementCore::Identity) +#endif diff --git a/server/identity/identitydialog.cpp b/server/identity/identitydialog.cpp index 2306d0c..f8e5865 100644 --- a/server/identity/identitydialog.cpp +++ b/server/identity/identitydialog.cpp @@ -1,628 +1,620 @@ /* identitydialog.cpp This file is part of KMail, the KDE mail client. SPDX-FileCopyrightText: 2002 Marc Mutz SPDX-FileCopyrightText: 2014-2023 Laurent Montel SPDX-License-Identifier: GPL-2.0-only */ #include "identitydialog.h" #include "identitymanager.h" #include "addressvalidationjob.h" #include "kleo_util.h" #include #include // other KMail headers: #include #include // other kdepim headers: -#include -#include +#include "identity/identity.h" +#include "identity/signatureconfigurator.h" #include // libkleopatra: #include #include #include #include // gpgme++ #include // other KDE headers: #include #include #include #include #include // Qt headers: #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // other headers: #include #include #include namespace KMail { class KeySelectionCombo : public Kleo::KeySelectionCombo { Q_OBJECT public: enum KeyType { SigningKey, EncryptionKey, }; explicit KeySelectionCombo(KeyType keyType, GpgME::Protocol protocol, QWidget *parent); ~KeySelectionCombo() override; void setIdentity(const QString &name, const QString &email); void init() override; private: void onCustomItemSelected(const QVariant &type); QString mEmail; QString mName; const KeyType mKeyType; const GpgME::Protocol mProtocol; }; class KeyGenerationJob : public QGpgME::Job { Q_OBJECT public: explicit KeyGenerationJob(const QString &name, const QString &email, KeySelectionCombo *parent); ~KeyGenerationJob() override; void slotCancel() override; void start(); private: void keyGenerated(const GpgME::KeyGenerationResult &result); const QString mName; const QString mEmail; QGpgME::Job *mJob = nullptr; }; KeyGenerationJob::KeyGenerationJob(const QString &name, const QString &email, KeySelectionCombo *parent) : QGpgME::Job(parent) , mName(name) , mEmail(email) { } KeyGenerationJob::~KeyGenerationJob() = default; void KeyGenerationJob::slotCancel() { if (mJob) { mJob->slotCancel(); } } void KeyGenerationJob::start() { auto job = new Kleo::DefaultKeyGenerationJob(this); connect(job, &Kleo::DefaultKeyGenerationJob::result, this, &KeyGenerationJob::keyGenerated); job->start(mEmail, mName); mJob = job; } void KeyGenerationJob::keyGenerated(const GpgME::KeyGenerationResult &result) { mJob = nullptr; if (result.error()) { KMessageBox::error(qobject_cast(parent()), i18n("Error while generating new key pair: %1", QString::fromUtf8(result.error().asString())), i18n("Key Generation Error")); Q_EMIT done(); return; } auto combo = qobject_cast(parent()); combo->setDefaultKey(QLatin1String(result.fingerprint())); connect(combo, &KeySelectionCombo::keyListingFinished, this, &KeyGenerationJob::done); combo->refreshKeys(); } KeySelectionCombo::KeySelectionCombo(KeyType keyType, GpgME::Protocol protocol, QWidget *parent) : Kleo::KeySelectionCombo(parent) , mKeyType(keyType) , mProtocol(protocol) { } KeySelectionCombo::~KeySelectionCombo() = default; void KeySelectionCombo::setIdentity(const QString &name, const QString &email) { mName = name; mEmail = email; setIdFilter(email); } void KeySelectionCombo::init() { Kleo::KeySelectionCombo::init(); std::shared_ptr keyFilter(new Kleo::DefaultKeyFilter); keyFilter->setIsOpenPGP(mProtocol == GpgME::OpenPGP ? Kleo::DefaultKeyFilter::Set : Kleo::DefaultKeyFilter::NotSet); if (mKeyType == SigningKey) { keyFilter->setCanSign(Kleo::DefaultKeyFilter::Set); keyFilter->setHasSecret(Kleo::DefaultKeyFilter::Set); } else { keyFilter->setCanEncrypt(Kleo::DefaultKeyFilter::Set); } setKeyFilter(keyFilter); prependCustomItem(QIcon(), i18n("No key"), QStringLiteral("no-key")); if (mProtocol == GpgME::OpenPGP) { appendCustomItem(QIcon::fromTheme(QStringLiteral("password-generate")), i18n("Generate a new key pair"), QStringLiteral("generate-new-key")); } connect(this, &KeySelectionCombo::customItemSelected, this, &KeySelectionCombo::onCustomItemSelected); } void KeySelectionCombo::onCustomItemSelected(const QVariant &type) { if (type == QLatin1String("no-key")) { return; } else if (type == QLatin1String("generate-new-key")) { auto job = new KeyGenerationJob(mName, mEmail, this); auto dlg = new Kleo::ProgressDialog(job, i18n("Generating new key pair..."), parentWidget()); dlg->setModal(true); setEnabled(false); connect(job, &KeyGenerationJob::done, this, [this]() { setEnabled(true); }); job->start(); } } IdentityDialog::IdentityDialog(QWidget *parent) : QDialog(parent) { setWindowTitle(i18nc("@title:window", "Edit Identity")); auto mainLayout = new QVBoxLayout(this); mainLayout->setContentsMargins({}); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, this, &IdentityDialog::slotAccepted); connect(buttonBox, &QDialogButtonBox::rejected, this, &IdentityDialog::reject); // // Tab Widget: General // auto page = new QWidget(this); mainLayout->addWidget(page); auto buttonBoxLayout = new QVBoxLayout; buttonBoxLayout->setContentsMargins(style()->pixelMetric(QStyle::PM_LayoutLeftMargin), style()->pixelMetric(QStyle::PM_LayoutTopMargin), style()->pixelMetric(QStyle::PM_LayoutRightMargin), style()->pixelMetric(QStyle::PM_LayoutBottomMargin)); buttonBoxLayout->addWidget(buttonBox); mainLayout->addLayout(buttonBoxLayout); auto vlay = new QVBoxLayout(page); vlay->setContentsMargins({}); mTabWidget = new QTabWidget(page); mTabWidget->tabBar()->setExpanding(true); mTabWidget->setDocumentMode(true); mTabWidget->setObjectName(QStringLiteral("config-identity-tab")); vlay->addWidget(mTabWidget); auto tab = new QWidget(mTabWidget); mTabWidget->addTab(tab, i18nc("@title:tab General identity settings.", "General")); auto formLayout = new QFormLayout(tab); // "Name" line edit and label: mNameEdit = new QLineEdit(tab); KLineEditEventHandler::catchReturnKey(mNameEdit); auto label = new QLabel(i18n("&Your name:"), tab); formLayout->addRow(label, mNameEdit); label->setBuddy(mNameEdit); QString msg = i18n( "

Your name

" "

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

" "

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

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

Organization

" "

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

" "

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

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

Email address

" "

This field should have your full email address.

" "

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

" "

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

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

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

" "

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

" "

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

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

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

" "

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

" "

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

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

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

" "

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

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

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

" "

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

"); label = new QLabel(i18n("S/MIME encryption certificate:"), mCryptographyTab); label->setBuddy(mSMIMEEncryptionKeyRequester); mSMIMEEncryptionKeyRequester->setWhatsThis(msg); connect(mSMIMEEncryptionKeyRequester, &KeySelectionCombo::keyListingFinished, this, [this] { slotKeyListingFinished(mSMIMEEncryptionKeyRequester); }); label->setWhatsThis(msg); formLayout->addRow(label, mSMIMEEncryptionKeyRequester); label->setEnabled(smimeProtocol); mSMIMEEncryptionKeyRequester->setEnabled(smimeProtocol); // "Preferred Crypto Message Format" combobox and label: mPreferredCryptoMessageFormat = new QComboBox(mCryptographyTab); QStringList l; l << Kleo::cryptoMessageFormatToLabel(Kleo::AutoFormat) << Kleo::cryptoMessageFormatToLabel(Kleo::InlineOpenPGPFormat) << Kleo::cryptoMessageFormatToLabel(Kleo::OpenPGPMIMEFormat) << Kleo::cryptoMessageFormatToLabel(Kleo::SMIMEFormat) << Kleo::cryptoMessageFormatToLabel(Kleo::SMIMEOpaqueFormat); mPreferredCryptoMessageFormat->addItems(l); label = new QLabel(i18nc("preferred format of encrypted messages", "Preferred format:"), mCryptographyTab); label->setBuddy(mPreferredCryptoMessageFormat); formLayout->addRow(label, mPreferredCryptoMessageFormat); mWarnNotEncrypt = new QCheckBox(i18n("Warn when trying to send unencrypted messages")); formLayout->addRow(QString(), mWarnNotEncrypt); // // Tab Widget: Signature // mSignatureConfigurator = new KIdentityManagementWidgets::SignatureConfigurator(mTabWidget); mTabWidget->addTab(mSignatureConfigurator, i18n("Signature")); } IdentityDialog::~IdentityDialog() = default; void IdentityDialog::slotAccepted() { // Validate email addresses const QString email = mEmailEdit->text().trimmed(); if (email.isEmpty()) { KMessageBox::error(this, i18n("You must provide an email for this identity."), i18nc("@title:window", "Empty Email Address")); return; } if (!KEmailAddress::isValidSimpleAddress(email)) { const QString errorMsg(KEmailAddress::simpleEmailAddressErrorMsg()); KMessageBox::error(this, errorMsg, i18n("Invalid Email Address")); return; } const GpgME::Key &pgpSigningKey = mPGPSigningKeyRequester->currentKey(); const GpgME::Key &pgpEncryptionKey = mPGPEncryptionKeyRequester->currentKey(); const GpgME::Key &smimeSigningKey = mSMIMESigningKeyRequester->currentKey(); const GpgME::Key &smimeEncryptionKey = mSMIMEEncryptionKeyRequester->currentKey(); QString msg; bool err = false; if (!keyMatchesEmailAddress(pgpSigningKey, email)) { msg = i18n( "One of the configured OpenPGP signing keys does not contain " "any user ID with the configured email address for this " "identity (%1).\n" "This might result in warning messages on the receiving side " "when trying to verify signatures made with this configuration.", email); err = true; } else if (!keyMatchesEmailAddress(pgpEncryptionKey, email)) { msg = i18n( "One of the configured OpenPGP encryption keys does not contain " "any user ID with the configured email address for this " "identity (%1).", email); err = true; } else if (!keyMatchesEmailAddress(smimeSigningKey, email)) { msg = i18n( "One of the configured S/MIME signing certificates does not contain " "the configured email address for this " "identity (%1).\n" "This might result in warning messages on the receiving side " "when trying to verify signatures made with this configuration.", email); err = true; } else if (!keyMatchesEmailAddress(smimeEncryptionKey, email)) { msg = i18n( "One of the configured S/MIME encryption certificates does not contain " "the configured email address for this " "identity (%1).", email); err = true; } if (err && KMessageBox::warningContinueCancel(this, msg, i18nc("@title:window", "Email Address Not Found in Key/Certificates"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QStringLiteral("warn_email_not_in_certificate")) != KMessageBox::Continue) { return; } - if (mSignatureConfigurator->isSignatureEnabled() && mSignatureConfigurator->signatureType() == Signature::FromFile) { - QFileInfo file(mSignatureConfigurator->filePath()); - if (!file.isReadable()) { - KMessageBox::error(this, i18n("The signature file is not valid")); - return; - } - } - accept(); } bool IdentityDialog::keyMatchesEmailAddress(const GpgME::Key &key, const QString &email_) { if (key.isNull()) { return true; } const QString email = email_.trimmed().toLower(); const auto uids = key.userIDs(); for (const auto &uid : uids) { QString em = QString::fromUtf8(uid.email() ? uid.email() : uid.id()); if (em.isEmpty()) { continue; } if (em[0] == QLatin1Char('<')) { em = em.mid(1, em.length() - 2); } if (em.toLower() == email) { return true; } } return false; } void IdentityDialog::setIdentity(KIdentityManagementCore::Identity &ident) { setWindowTitle(i18nc("@title:window", "Edit Identity \"%1\"", ident.identityName())); // "General" tab: mNameEdit->setText(ident.fullName()); mOrganizationEdit->setText(ident.organization()); mEmailEdit->setText(ident.primaryEmailAddress()); mDictionaryCombo->setCurrentByDictionaryName(ident.dictionary()); // "Cryptography" tab: mPGPSigningKeyRequester->setDefaultKey(QLatin1String(ident.pgpSigningKey())); mPGPEncryptionKeyRequester->setDefaultKey(QLatin1String(ident.pgpEncryptionKey())); mPGPSameKey->setChecked(ident.pgpSigningKey() == ident.pgpEncryptionKey()); mSMIMESigningKeyRequester->setDefaultKey(QLatin1String(ident.smimeSigningKey())); mSMIMEEncryptionKeyRequester->setDefaultKey(QLatin1String(ident.smimeEncryptionKey())); mPreferredCryptoMessageFormat->setCurrentIndex(format2cb(Kleo::stringToCryptoMessageFormat(ident.preferredCryptoMessageFormat()))); mWarnNotEncrypt->setChecked(ident.warnNotEncrypt()); // "Signature" tab: mSignatureConfigurator->setImageLocation(ident); mSignatureConfigurator->setSignature(ident.signature()); // set the configured email address as initial query of the key // requesters: const QString name = mNameEdit->text().trimmed(); const QString email = mEmailEdit->text().trimmed(); mPGPEncryptionKeyRequester->setIdentity(name, email); mPGPSigningKeyRequester->setIdentity(name, email); mSMIMEEncryptionKeyRequester->setIdentity(name, email); mSMIMESigningKeyRequester->setIdentity(name, email); } void IdentityDialog::updateIdentity(KIdentityManagementCore::Identity &ident) { // "General" tab: ident.setFullName(mNameEdit->text()); ident.setOrganization(mOrganizationEdit->text()); QString email = mEmailEdit->text().trimmed(); ident.setPrimaryEmailAddress(email); // "Cryptography" tab: ident.setPGPSigningKey(mPGPSigningKeyRequester->currentKey().primaryFingerprint()); ident.setPGPEncryptionKey(mPGPEncryptionKeyRequester->currentKey().primaryFingerprint()); ident.setSMIMESigningKey(mSMIMESigningKeyRequester->currentKey().primaryFingerprint()); ident.setSMIMEEncryptionKey(mSMIMEEncryptionKeyRequester->currentKey().primaryFingerprint()); ident.setPreferredCryptoMessageFormat(QLatin1String(Kleo::cryptoMessageFormatToString(cb2format(mPreferredCryptoMessageFormat->currentIndex())))); ident.setEncryptionOverride(true); ident.setWarnNotEncrypt(mWarnNotEncrypt->isChecked()); ident.setWarnNotEncrypt(mWarnNotEncrypt->isChecked()); // "Advanced" tab: ident.setDictionary(mDictionaryCombo->currentDictionaryName()); // "Signature" tab: ident.setSignature(mSignatureConfigurator->signature()); } void IdentityDialog::slotKeyListingFinished(KeySelectionCombo *combo) { mInitialLoadingFinished << combo; if (mInitialLoadingFinished.count() == 2) { Q_EMIT keyListingFinished(); } } } #include "identitydialog.moc" #include "moc_identitydialog.cpp" diff --git a/server/identity/identitymanager.h b/server/identity/identitymanager.h index 8c4302d..3168439 100644 --- a/server/identity/identitymanager.h +++ b/server/identity/identitymanager.h @@ -1,40 +1,40 @@ // SPDX-FileCopyrightText: 2023 g10 code Gmbh // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include +#include "identity.h" 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: QStringList groupList(KConfig *config) const; IdentityManager(); ~IdentityManager(); KIdentityManagementCore::Identity::List mIdentities; KConfig *mConfig = nullptr; }; \ No newline at end of file diff --git a/server/identity/signature.cpp b/server/identity/signature.cpp new file mode 100644 index 0000000..c1cd9ff --- /dev/null +++ b/server/identity/signature.cpp @@ -0,0 +1,408 @@ +/* + SPDX-FileCopyrightText: 2002-2004 Marc Mutz + SPDX-FileCopyrightText: 2007 Tom Albers + SPDX-FileCopyrightText: 2009 Thomas McGuire + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "signature.h" + +#include "editor_debug.h" +#include +#include +#include + +#include +#include +#include + +#include + +using namespace KIdentityManagementCore; + +class KIdentityManagementCore::SignaturePrivate +{ +public: + explicit SignaturePrivate(Signature *qq) + : q(qq) + { + } + + void assignFrom(const KIdentityManagementCore::Signature &that); + void cleanupImages(); + void saveImages() const; + + /// List of images that belong to this signature. Either added by addImage() or + /// by readConfig(). + QList embeddedImages; + + /// The directory where the images will be saved to. + QString saveLocation; + QString text; + Signature::Type type = Signature::Disabled; + bool enabled = false; + bool inlinedHtml = false; + Signature *const q; +}; + +// Returns the names of all images in the HTML code +static QStringList findImageNames(const QString &htmlCode) +{ + QStringList imageNames; + QTextDocument doc; + doc.setHtml(htmlCode); + for (auto block = doc.begin(); block.isValid(); block = block.next()) { + for (auto it = block.begin(); !it.atEnd(); ++it) { + const auto fragment = it.fragment(); + if (fragment.isValid()) { + const auto imageFormat = fragment.charFormat().toImageFormat(); + if (imageFormat.isValid() && !imageFormat.name().startsWith(QLatin1StringView("http")) && !imageNames.contains(imageFormat.name())) { + imageNames.push_back(imageFormat.name()); + } + } + } + } + return imageNames; +} + +void SignaturePrivate::assignFrom(const KIdentityManagementCore::Signature &that) +{ + inlinedHtml = that.isInlinedHtml(); + text = that.text(); + type = that.type(); + enabled = that.isEnabledSignature(); + saveLocation = that.imageLocation(); + embeddedImages = that.embeddedImages(); +} + +void SignaturePrivate::cleanupImages() +{ + // Remove any images from the internal structure that are no longer there + if (inlinedHtml) { + auto it = std::remove_if(embeddedImages.begin(), embeddedImages.end(), [this](const Signature::EmbeddedImagePtr &imageInList) { + const QStringList lstImage = findImageNames(text); + for (const QString &imageInHtml : lstImage) { + if (imageInHtml == imageInList->name) { + return false; + } + } + return true; + }); + embeddedImages.erase(it, embeddedImages.end()); + } + + // Delete all the old image files + if (!saveLocation.isEmpty()) { + QDir dir(saveLocation); + const QStringList lst = dir.entryList(QDir::Files | QDir::NoDotAndDotDot | QDir::NoSymLinks); + for (const QString &fileName : lst) { + if (fileName.endsWith(QLatin1StringView(".png"), Qt::CaseInsensitive)) { + qCDebug(EDITOR_LOG) << "Deleting old image" << dir.path() + fileName; + dir.remove(fileName); + } + } + } +} + +void SignaturePrivate::saveImages() const +{ + if (inlinedHtml && !saveLocation.isEmpty()) { + for (const Signature::EmbeddedImagePtr &image : std::as_const(embeddedImages)) { + const QString location = saveLocation + QLatin1Char('/') + image->name; + if (!image->image.save(location, "PNG")) { + qCWarning(EDITOR_LOG) << "Failed to save image" << location; + } + } + } +} + +QDataStream &operator<<(QDataStream &stream, const KIdentityManagementCore::Signature::EmbeddedImagePtr &img) +{ + return stream << img->image << img->name; +} + +QDataStream &operator>>(QDataStream &stream, const KIdentityManagementCore::Signature::EmbeddedImagePtr &img) +{ + return stream >> img->image >> img->name; +} + +Signature::Signature() + : d(new SignaturePrivate(this)) +{ + d->type = Disabled; + d->inlinedHtml = false; +} + +Signature::Signature(const QString &text) + : d(new SignaturePrivate(this)) +{ + d->type = Inlined; + d->inlinedHtml = false; + d->text = text; +} + +Signature::Signature(const Signature &that) + : d(new SignaturePrivate(this)) +{ + d->assignFrom(that); +} + +Signature &Signature::operator=(const KIdentityManagementCore::Signature &that) +{ + if (this == &that) { + return *this; + } + + d->assignFrom(that); + return *this; +} + +Signature::~Signature() = default; + +QString Signature::rawText(bool *ok, QString *errorMessage) const +{ + switch (d->type) { + case Disabled: + if (ok) { + *ok = true; + } + return {}; + case Inlined: + if (ok) { + *ok = true; + } + return d->text; + } + qCritical() << "Signature::type() returned unknown value!"; + return {}; // make compiler happy +} + +QString Signature::withSeparator(bool *ok, QString *errorMessage) const +{ + QString signature = rawText(ok, errorMessage); + if (ok && (*ok) == false) { + return {}; + } + + if (signature.isEmpty()) { + return signature; // don't add a separator in this case + } + + const bool htmlSig = (isInlinedHtml() && d->type == Inlined); + QString newline = htmlSig ? QStringLiteral("
") : QStringLiteral("\n"); + if (htmlSig && signature.startsWith(QLatin1StringView("inlinedHtml = isHtml; +} + +bool Signature::isInlinedHtml() const +{ + return d->inlinedHtml; +} + +// config keys and values: +static const char sigTypeKey[] = "Signature Type"; +static const char sigTypeInlineValue[] = "inline"; +static const char sigTypeFileValue[] = "file"; +static const char sigTypeCommandValue[] = "command"; +static const char sigTypeDisabledValue[] = "disabled"; +static const char sigTextKey[] = "Inline Signature"; +static const char sigFileKey[] = "Signature File"; +static const char sigCommandKey[] = "Signature Command"; +static const char sigTypeInlinedHtmlKey[] = "Inlined Html"; +static const char sigImageLocation[] = "Image Location"; +static const char sigEnabled[] = "Signature Enabled"; + +void Signature::readConfig(const KConfigGroup &config) +{ + QString sigType = config.readEntry(sigTypeKey); + if (sigType == QLatin1StringView(sigTypeInlineValue)) { + d->type = Inlined; + d->inlinedHtml = config.readEntry(sigTypeInlinedHtmlKey, false); + } else if (sigType == QLatin1StringView(sigTypeDisabledValue)) { + d->enabled = false; + } + if (d->type != Disabled) { + d->enabled = config.readEntry(sigEnabled, true); + } + + d->text = config.readEntry(sigTextKey); + d->saveLocation = config.readEntry(sigImageLocation); + + if (isInlinedHtml() && !d->saveLocation.isEmpty()) { + QDir dir(d->saveLocation); + const QStringList lst = dir.entryList(QDir::Files | QDir::NoDotAndDotDot | QDir::NoSymLinks); + for (const QString &fileName : lst) { + if (fileName.endsWith(QLatin1StringView(".png"), Qt::CaseInsensitive)) { + QImage image; + if (image.load(dir.path() + QLatin1Char('/') + fileName)) { + addImage(image, fileName); + } else { + qCWarning(EDITOR_LOG) << "Unable to load image" << dir.path() + QLatin1Char('/') + fileName; + } + } + } + } +} + +void Signature::writeConfig(KConfigGroup &config) const +{ + switch (d->type) { + case Inlined: + config.writeEntry(sigTypeKey, sigTypeInlineValue); + config.writeEntry(sigTypeInlinedHtmlKey, d->inlinedHtml); + break; + default: + break; + } + config.writeEntry(sigTextKey, d->text); + config.writeEntry(sigImageLocation, d->saveLocation); + config.writeEntry(sigEnabled, d->enabled); + + d->cleanupImages(); + d->saveImages(); +} + +QList Signature::embeddedImages() const +{ + return d->embeddedImages; +} + +void Signature::setEmbeddedImages(const QList &embedded) +{ + d->embeddedImages = embedded; +} + +// --------------------- Operators -------------------// + +QDataStream &KIdentityManagementCore::operator<<(QDataStream &stream, const KIdentityManagementCore::Signature &sig) +{ + return stream << static_cast(sig.type()) << sig.text() << sig.imageLocation() << sig.embeddedImages() << sig.isEnabledSignature(); +} + +QDataStream &KIdentityManagementCore::operator>>(QDataStream &stream, KIdentityManagementCore::Signature &sig) +{ + quint8 s; + QString path; + QString text; + QString saveLocation; + QList lst; + bool enabled; + stream >> s >> path >> text >> saveLocation >> lst >> enabled; + sig.setText(text); + sig.setImageLocation(saveLocation); + sig.setEmbeddedImages(lst); + sig.setEnabledSignature(enabled); + sig.setType(static_cast(s)); + return stream; +} + +bool Signature::operator==(const Signature &other) const +{ + if (d->type != other.type()) { + return false; + } + + if (d->enabled != other.isEnabledSignature()) { + return false; + } + + if (d->type == Inlined && d->inlinedHtml) { + if (d->saveLocation != other.imageLocation()) { + return false; + } + if (d->embeddedImages != other.embeddedImages()) { + return false; + } + } + + switch (d->type) { + case Inlined: + return d->text == other.text(); + default: + case Disabled: + return true; + } +} + +QString Signature::toPlainText() const +{ + QString sigText = rawText(); + if (!sigText.isEmpty() && isInlinedHtml() && type() == Inlined) { + // Use a QTextDocument as a helper, it does all the work for us and + // strips all HTML tags. + QTextDocument helper; + QTextCursor helperCursor(&helper); + helperCursor.insertHtml(sigText); + sigText = helper.toPlainText(); + } + return sigText; +} + +void Signature::addImage(const QImage &imageData, const QString &imageName) +{ + Q_ASSERT(!(d->saveLocation.isEmpty())); + Signature::EmbeddedImagePtr image(new Signature::EmbeddedImage()); + image->image = imageData; + image->name = imageName; + d->embeddedImages.append(image); +} + +void Signature::setImageLocation(const QString &path) +{ + d->saveLocation = path; +} + +QString Signature::imageLocation() const +{ + return d->saveLocation; +} + +// --------------- Getters -----------------------// + +QString Signature::text() const +{ + return d->text; +} + +Signature::Type Signature::type() const +{ + return d->type; +} + +// --------------- Setters -----------------------// + +void Signature::setText(const QString &text) +{ + d->text = text; + d->type = Inlined; +} + +void Signature::setType(Type type) +{ + d->type = type; +} + +void Signature::setEnabledSignature(bool enabled) +{ + d->enabled = enabled; +} + +bool Signature::isEnabledSignature() const +{ + return d->enabled; +} diff --git a/server/identity/signature.h b/server/identity/signature.h new file mode 100644 index 0000000..c83c4b8 --- /dev/null +++ b/server/identity/signature.h @@ -0,0 +1,206 @@ +/* + SPDX-FileCopyrightText: 2002-2004 Marc Mutz + SPDX-FileCopyrightText: 2007 Tom Albers + SPDX-FileCopyrightText: 2009 Thomas McGuire + Author: Stefan Taferner + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include +#include +#include +#include + +class KConfigGroup; +namespace KIdentityManagementCore +{ +class Signature; +class Identity; +class SignaturePrivate; +QDataStream &operator<<(QDataStream &stream, const KIdentityManagementCore::Signature &sig); +QDataStream &operator>>(QDataStream &stream, KIdentityManagementCore::Signature &sig); + +/** + * @short Abstraction of a signature (aka "footer"). + * + * The signature can either be plain text, HTML text, text returned from a command or text stored + * in a file. + * + * In case of HTML text, the signature can contain images. + * Since you set the HTML source with setText(), there also needs to be a way to add the images + * to the signature, as the HTML source contains only the img tags that reference those images. + * To add the image to the signature, call addImage(). The name given there must match the name + * of the img tag in the HTML source. + * + * The images need to be stored somewhere. The Signature class handles that by storing all images + * in a directory. You must set that directory with setImageLocation(), before calling addImage(). + * The images added with addImage() are then saved to that directory when calling writeConfig(). + * When loading a signature, readConfig() automatically loads the images as well. + * To actually add the images to a text edit, call insertIntoTextEdit(). + * + * Example of creating a HTML signature and then inserting it into a text edit: + * @code + * Signature htmlSig; + * htmlSig.setText( " World" ); + * htmlSig.setInlinedHtml( true ); + * htmlSig.setImageLocation( KStandardDirs::locateLocal( "data", "emailidentities/example/" ); + * QImage image = ...; + * htmlSig.addImage( image, "hello.png" ); + * ... + * KTextEdit edit; + * htmlSig.insertIntoTextEdit( KIdentityManagementCore::Signature::End, + * KIdentityManagementCore::Signature::AddSeparator, &edit ); + * @endcode + */ +class Signature +{ + friend class Identity; + + friend QDataStream &operator<<(QDataStream &stream, const Signature &sig); + friend QDataStream &operator>>(QDataStream &stream, Signature &sig); + +public: + /** Type of signature (ie. way to obtain the signature text) */ + enum Type { Disabled = 0, Inlined = 1, }; + + /** + * Describes the placement of the signature text when it is to be inserted into a + * text edit + */ + enum Placement { + Start, ///< The signature is placed at the start of the textedit + End, ///< The signature is placed at the end of the textedit + AtCursor ///< The signature is placed at the current cursor position + }; + + struct EmbeddedImage { + QImage image; + QString name; + }; + using EmbeddedImagePtr = QSharedPointer; + + /** Used for comparison */ + bool operator==(const Signature &other) const; + + /** Constructor for disabled signature */ + Signature(); + /** Constructor for inline text */ + Signature(const QString &text); + /** Copy constructor */ + Signature(const Signature &that); + /** Assignment operator */ + Signature &operator=(const Signature &that); + /** Destructor */ + ~Signature(); + + /** @return the raw signature text as entered resp. read from file. + @param ok set to @c true if reading succeeded + @param errorMessage If available, contains a human readable explanation for @p ok being @c false. + */ + [[nodiscard]] QString rawText(bool *ok = nullptr, QString *errorMessage = nullptr) const; + + /** @return the signature text with a "-- \n" separator added, if + necessary. A newline will not be appended or prepended. + @param ok set to @c true if reading succeeded + @param errorMessage If available, contains a human readable explanation for @p ok being @c false. + */ + [[nodiscard]] QString withSeparator(bool *ok = nullptr, QString *errorMessage = nullptr) const; + + /** Set the signature text and mark this signature as being of + "inline text" type. */ + void setText(const QString &text); + [[nodiscard]] QString text() const; + + /** + * Returns the text of the signature. If the signature is HTML, the HTML + * tags will be stripped. + * @since 4.4 + */ + [[nodiscard]] QString toPlainText() const; + + /// @return the type of signature (ie. way to obtain the signature text) + [[nodiscard]] Type type() const; + void setType(Type type); + + /** + * Sets the inlined signature to text or html + * @param isHtml sets the inlined signature to html + * @since 4.1 + */ + void setInlinedHtml(bool isHtml); + + /** + * @return boolean whether the inlined signature is html + * @since 4.1 + */ + [[nodiscard]] bool isInlinedHtml() const; + + /** + * Sets the location where the copies of the signature images will be stored. + * The images will be stored there when calling writeConfig(). The image location + * is stored in the config, so the next readConfig() call knows where to look for + * images. + * It is recommended to use KStandardDirs::locateLocal( "data", "emailidentities/%1" ) + * for the location, where %1 is the unique identifier of the identity. + * + * @warning readConfig will delete all other PNG files in this directory, as they could + * be stale inline image files + * + * Like with addImage(), the SignatureConfigurator will handle this for you. + * @param path the path to set as image location + * @since 4.4 + */ + void setImageLocation(const QString &path); + [[nodiscard]] QString imageLocation() const; + + /** + * Adds the given image to the signature. + * This is needed if you use setText() to set some HTML source that references images. Those + * referenced images needed to be added by calling this function. The @imageName has to match + * the src attribute of the img tag. + * + * If you use SignatureConfigurator, you don't need to call this function, as the configurator + * will handle this for you. + * setImageLocation() needs to be called once before. + * @since 4.4 + */ + void addImage(const QImage &image, const QString &imageName); + + /** + * @brief setEnabledSignature + * @param enabled enables signature if set as @c true + * @since 4.9 + */ + void setEnabledSignature(bool enabled); + [[nodiscard]] bool isEnabledSignature() const; + + enum AddedTextFlag { + AddNothing = 0, ///< Don't add any text to the signature + AddSeparator = 1 << 0, ///< The separator '-- \n' will be added in front + /// of the signature + AddNewLines = 1 << 1 ///< Add a newline character in front or after the signature, + /// depending on the placement + }; + + /// Describes which additional parts should be added to the signature + using AddedText = QFlags; + + [[nodiscard]] QList embeddedImages() const; + void setEmbeddedImages(const QList &embedded); + +protected: + // TODO: KDE5: BIC: Move all to private class + void writeConfig(KConfigGroup &config) const; + void readConfig(const KConfigGroup &config); + +private: + //@cond PRIVATE + std::unique_ptr const d; + //@endcond +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(Signature::AddedText) +} diff --git a/server/identity/signatureconfigurator.cpp b/server/identity/signatureconfigurator.cpp new file mode 100644 index 0000000..f50b06e --- /dev/null +++ b/server/identity/signatureconfigurator.cpp @@ -0,0 +1,299 @@ +/* -*- c++ -*- + SPDX-FileCopyrightText: 2008 Thomas McGuire + SPDX-FileCopyrightText: 2008 Edwin Schepers + SPDX-FileCopyrightText: 2004 Marc Mutz + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#include "signatureconfigurator.h" +#include "identity.h" +#include "signaturerichtexteditor_p.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include + +using namespace KIdentityManagementWidgets; + +namespace KIdentityManagementWidgets +{ +/** + Private class that helps to provide binary compatibility between releases. + @internal + */ +//@cond PRIVATE +class Q_DECL_HIDDEN SignatureConfiguratorPrivate +{ +public: + explicit SignatureConfiguratorPrivate(SignatureConfigurator *parent); + void init(); + // Returns the current text of the textedit as HTML code, but strips + // unnecessary tags Qt inserts + [[nodiscard]] QString asCleanedHTML() const; + + QString imageLocation; + SignatureConfigurator *const q; + QCheckBox *mEnableCheck = nullptr; + QCheckBox *mHtmlCheck = nullptr; + QPushButton *mEditButton = nullptr; + KToolBar *mEditToolBar = nullptr; + KToolBar *mFormatToolBar = nullptr; + KPIMTextEdit::RichTextComposer *mTextEdit = nullptr; + bool inlinedHtml = false; +}; +//@endcond + +SignatureConfiguratorPrivate::SignatureConfiguratorPrivate(SignatureConfigurator *parent) + : q(parent) + , inlinedHtml(true) +{ +} + +QString SignatureConfiguratorPrivate::asCleanedHTML() const +{ + QString text = mTextEdit->toHtml(); + + // Beautiful little hack to find the html headers produced by Qt. + QTextDocument textDocument; + const QString html = textDocument.toHtml(); + + // Now remove each line from the text, the result is clean html. + const QStringList lst = html.split(QLatin1Char('\n')); + for (const QString &line : lst) { + text.remove(line + QLatin1Char('\n')); + } + return text; +} + +void SignatureConfiguratorPrivate::init() +{ + auto vlay = new QVBoxLayout(q); + vlay->setObjectName(QLatin1StringView("main layout")); + + // "enable signature" checkbox: + mEnableCheck = new QCheckBox(i18n("&Enable signature"), q); + mEnableCheck->setWhatsThis( + i18n("Check this box if you want KMail to append a signature to mails " + "written with this identity.")); + vlay->addWidget(mEnableCheck); + + // "obtain signature text from" combo and label: + auto hlay = new QHBoxLayout(); // inherits spacing + vlay->addLayout(hlay); + + // widget stack that is controlled by the source combo: + auto widgetStack = new QStackedWidget(q); + widgetStack->setEnabled(false); // since !mEnableCheck->isChecked() + vlay->addWidget(widgetStack, 1); + // connects for the enabling of the widgets depending on + // signatureEnabled: + q->connect(mEnableCheck, &QCheckBox::toggled, widgetStack, &QStackedWidget::setEnabled); + // The focus might be still in the widget that is disabled + q->connect(mEnableCheck, &QCheckBox::clicked, mEnableCheck, qOverload<>(&QCheckBox::setFocus)); + + int pageno = 0; + // page 0: input field for direct entering: + auto page = new QWidget(widgetStack); + widgetStack->insertWidget(pageno, page); + auto page_vlay = new QVBoxLayout(page); + page_vlay->setContentsMargins(0, 0, 0, 0); + +#ifndef QT_NO_TOOLBAR + mEditToolBar = new KToolBar(q); + mEditToolBar->setToolButtonStyle(Qt::ToolButtonIconOnly); + page_vlay->addWidget(mEditToolBar, 0); + + mFormatToolBar = new KToolBar(q); + mFormatToolBar->setToolButtonStyle(Qt::ToolButtonIconOnly); + page_vlay->addWidget(mFormatToolBar, 1); +#endif + + mTextEdit = new KPIMTextEdit::RichTextComposer(q); + + auto richTextEditorwidget = new TextCustomEditor::RichTextEditorWidget(mTextEdit, q); + page_vlay->addWidget(richTextEditorwidget, 2); + mTextEdit->setWhatsThis(i18n("Use this field to enter an arbitrary static signature.")); + + // Fill the toolbars. + auto actionCollection = new KActionCollection(q); + mTextEdit->createActions(actionCollection); +#ifndef QT_NO_TOOLBAR + mEditToolBar->addAction(actionCollection->action(QStringLiteral("format_text_bold"))); + mEditToolBar->addAction(actionCollection->action(QStringLiteral("format_text_italic"))); + mEditToolBar->addAction(actionCollection->action(QStringLiteral("format_text_underline"))); + mEditToolBar->addAction(actionCollection->action(QStringLiteral("format_text_strikeout"))); + mEditToolBar->addAction(actionCollection->action(QStringLiteral("format_text_foreground_color"))); + mEditToolBar->addAction(actionCollection->action(QStringLiteral("format_text_background_color"))); + mEditToolBar->addAction(actionCollection->action(QStringLiteral("format_font_family"))); + mEditToolBar->addAction(actionCollection->action(QStringLiteral("format_font_size"))); + mEditToolBar->addAction(actionCollection->action(QStringLiteral("format_reset"))); + + mFormatToolBar->addAction(actionCollection->action(QStringLiteral("format_list_style"))); + mFormatToolBar->addAction(actionCollection->action(QStringLiteral("format_list_indent_more"))); + mFormatToolBar->addAction(actionCollection->action(QStringLiteral("format_list_indent_less"))); + mFormatToolBar->addAction(actionCollection->action(QStringLiteral("format_list_indent_less"))); + mFormatToolBar->addSeparator(); + + mFormatToolBar->addAction(actionCollection->action(QStringLiteral("format_align_left"))); + mFormatToolBar->addAction(actionCollection->action(QStringLiteral("format_align_center"))); + mFormatToolBar->addAction(actionCollection->action(QStringLiteral("format_align_right"))); + mFormatToolBar->addAction(actionCollection->action(QStringLiteral("format_align_justify"))); + mFormatToolBar->addSeparator(); + + mFormatToolBar->addAction(actionCollection->action(QStringLiteral("insert_horizontal_rule"))); + mFormatToolBar->addAction(actionCollection->action(QStringLiteral("manage_link"))); + mFormatToolBar->addAction(actionCollection->action(QStringLiteral("format_painter"))); + + mFormatToolBar->addSeparator(); + mFormatToolBar->addAction(actionCollection->action(QStringLiteral("add_image"))); + mFormatToolBar->addSeparator(); + mFormatToolBar->addAction(actionCollection->action(QStringLiteral("insert_html"))); + mFormatToolBar->addAction(actionCollection->action(QStringLiteral("insert_table"))); +#endif + + hlay = new QHBoxLayout(); // inherits spacing + page_vlay->addLayout(hlay); + mHtmlCheck = new QCheckBox(i18n("&Use HTML"), page); + q->connect(mHtmlCheck, &QCheckBox::clicked, q, &SignatureConfigurator::slotSetHtml); + hlay->addWidget(mHtmlCheck); + inlinedHtml = true; + + widgetStack->setCurrentIndex(0); // since mSourceCombo->currentItem() == 0 +} + +SignatureConfigurator::SignatureConfigurator(QWidget *parent) + : QWidget(parent) + , d(new SignatureConfiguratorPrivate(this)) +{ + d->init(); +} + +SignatureConfigurator::~SignatureConfigurator() = default; + +bool SignatureConfigurator::isSignatureEnabled() const +{ + return d->mEnableCheck->isChecked(); +} + +void SignatureConfigurator::setSignatureEnabled(bool enable) +{ + d->mEnableCheck->setChecked(enable); +} + +Signature::Type SignatureConfigurator::signatureType() const +{ + return Signature::Inlined; +} + +void SignatureConfigurator::setInlineText(const QString &text) +{ + d->mTextEdit->setTextOrHtml(text); +} + +Signature SignatureConfigurator::signature() const +{ + Signature sig; + const Signature::Type sigType = signatureType(); + switch (sigType) { + case Signature::Inlined: + sig.setInlinedHtml(d->inlinedHtml); + sig.setText(d->inlinedHtml ? d->asCleanedHTML() : d->mTextEdit->textOrHtml()); + if (d->inlinedHtml) { + if (!d->imageLocation.isEmpty()) { + sig.setImageLocation(d->imageLocation); + } + const KPIMTextEdit::ImageWithNameList images = d->mTextEdit->composerControler()->composerImages()->imagesWithName(); + for (const KPIMTextEdit::ImageWithNamePtr &image : images) { + sig.addImage(image->image, image->name); + } + } + break; + case Signature::Disabled: + /* do nothing */ + break; + } + sig.setEnabledSignature(isSignatureEnabled()); + sig.setType(sigType); + return sig; +} + +void SignatureConfigurator::setSignature(const Signature &sig) +{ + setSignatureEnabled(sig.isEnabledSignature()); + + if (sig.isInlinedHtml()) { + d->mHtmlCheck->setCheckState(Qt::Checked); + } else { + d->mHtmlCheck->setCheckState(Qt::Unchecked); + } + slotSetHtml(); + + // Let insertIntoTextEdit() handle setting the text, as that function also adds the images. + d->mTextEdit->clear(); + SignatureRichTextEditor::insertIntoTextEdit(sig, Signature::Start, Signature::AddNothing, d->mTextEdit, true); +} + +// "use HTML"-checkbox (un)checked +void SignatureConfigurator::slotSetHtml() +{ + if (d->mHtmlCheck->checkState() == Qt::Unchecked) { + d->mHtmlCheck->setText(i18n("&Use HTML")); +#ifndef QT_NO_TOOLBAR + d->mEditToolBar->setVisible(false); + d->mEditToolBar->setEnabled(false); + d->mFormatToolBar->setVisible(false); + d->mFormatToolBar->setEnabled(false); +#endif + d->mTextEdit->switchToPlainText(); + d->inlinedHtml = false; + } else { + d->mHtmlCheck->setText(i18n("&Use HTML (disabling removes formatting)")); + d->inlinedHtml = true; +#ifndef QT_NO_TOOLBAR + d->mEditToolBar->setVisible(true); + d->mEditToolBar->setEnabled(true); + d->mFormatToolBar->setVisible(true); + d->mFormatToolBar->setEnabled(true); +#endif + d->mTextEdit->activateRichText(); + } +} + +void SignatureConfigurator::setImageLocation(const QString &path) +{ + d->imageLocation = path; +} + +void SignatureConfigurator::setImageLocation(const KIdentityManagementCore::Identity &identity) +{ + const QString dir = + QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/emailidentities/%1/").arg(QString::number(identity.uoid())); + QDir().mkpath(dir); + setImageLocation(dir); +} +} + +#include "moc_signatureconfigurator.cpp" diff --git a/server/identity/signatureconfigurator.h b/server/identity/signatureconfigurator.h new file mode 100644 index 0000000..40e2be1 --- /dev/null +++ b/server/identity/signatureconfigurator.h @@ -0,0 +1,132 @@ +/* -*- c++ -*- + SPDX-FileCopyrightText: 2008 Thomas McGuire + SPDX-FileCopyrightText: 2008 Edwin Schepers + SPDX-FileCopyrightText: 2008 Tom Albers + SPDX-FileCopyrightText: 2004 Marc Mutz + + SPDX-License-Identifier: LGPL-2.1-or-later +*/ + +#pragma once + +#include "identity/signature.h" // for Signature::Type +#include +#include + +using KIdentityManagementCore::Signature; + +namespace KIdentityManagementWidgets +{ +class SignatureConfiguratorPrivate; +/** + * This widget gives an interface so users can edit their signature. + * You can set a signature via setSignature(), let the user edit the + * signature and when done, read the signature back. + */ +class SignatureConfigurator : public QWidget +{ + Q_OBJECT +public: + /** + * Constructor + */ + explicit SignatureConfigurator(QWidget *parent = nullptr); + + /** + * destructor + */ + ~SignatureConfigurator() override; + + /** + * Enum for the different viewmodes. + */ + enum ViewMode { ShowCode, ShowHtml }; + + /** + * Indicated if the user wants a signature + */ + [[nodiscard]] bool isSignatureEnabled() const; + + /** + * Use this to activate the signature. + */ + void setSignatureEnabled(bool enable); + + /** + * This returns the type of the signature, + * so that can be Disabled, Inline, fromFile, etc. + */ + [[nodiscard]] Signature::Type signatureType() const; + + /** + * Make @p text the text for the signature. + */ + void setInlineText(const QString &text); + + /** + * Returns the file url which the user wants + * to use as a signature. + */ + [[nodiscard]] QString filePath() const; + + /** + * Set @p url for the file url part of the + * widget. + */ + void setFileURL(const QString &url); + + /** + * Returns the url of the command which the + * users wants to use as signature. + */ + [[nodiscard]] QString commandPath() const; + + /** + * Sets @p url as the command to execute. + */ + void setCommandURL(const QString &url); + + /** + Convenience method. + @return a Signature object representing the state of the widgets. + **/ + [[nodiscard]] Signature signature() const; + + /** + Convenience method. Sets the widgets according to @p sig + @param sig the signature to configure + **/ + void setSignature(const Signature &sig); + + /** + * Sets the directory where the images used in the HTML signature will be stored. + * Needs to be called before calling setSignature(), as each signature should use + * a different location. + * The directory needs to exist, it will not be created. + * @param path the image location to set + * @since 4.4 + * @sa Signature::setImageLocation + */ + void setImageLocation(const QString &path); + + /** + * Sets the image location to the image location of a given identity, which is + * emailidentities//. + * + * @param identity The identity whose unique ID will be used to determine the image + * location. + * @since 4.4 + */ + void setImageLocation(const KIdentityManagementCore::Identity &identity); + +private: + void slotUrlChanged(); + void slotEdit(); + void slotSetHtml(); + + //@cond PRIVATE + friend class SignatureConfiguratorPrivate; + std::unique_ptr const d; + //@endcond +}; +} diff --git a/server/identity/signaturerichtexteditor.cpp b/server/identity/signaturerichtexteditor.cpp new file mode 100644 index 0000000..feb6003 --- /dev/null +++ b/server/identity/signaturerichtexteditor.cpp @@ -0,0 +1,141 @@ +/* + SPDX-FileCopyrightText: 2002-2004 Marc Mutz + SPDX-FileCopyrightText: 2007 Tom Albers + SPDX-FileCopyrightText: 2009 Thomas McGuire + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "signaturerichtexteditor_p.h" + +#include +#include +#include + +using namespace KIdentityManagementWidgets; + +static bool isCursorAtEndOfLine(const QTextCursor &cursor) +{ + QTextCursor testCursor = cursor; + testCursor.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor); + return !testCursor.hasSelection(); +} + +static void insertSignatureHelper(const QString &signature, + KPIMTextEdit::RichTextComposer *textEdit, + KIdentityManagementCore::Signature::Placement placement, + bool isHtml, + bool addNewlines) +{ + if (!signature.isEmpty()) { + // Save the modified state of the document, as inserting a signature + // shouldn't change this. Restore it at the end of this function. + bool isModified = textEdit->document()->isModified(); + + // Move to the desired position, where the signature should be inserted + QTextCursor cursor = textEdit->textCursor(); + QTextCursor oldCursor = cursor; + cursor.beginEditBlock(); + + if (placement == KIdentityManagementCore::Signature::End) { + cursor.movePosition(QTextCursor::End); + } else if (placement == KIdentityManagementCore::Signature::Start) { + cursor.movePosition(QTextCursor::Start); + } else if (placement == KIdentityManagementCore::Signature::AtCursor) { + cursor.movePosition(QTextCursor::StartOfLine); + } + textEdit->setTextCursor(cursor); + + QString lineSep; + if (addNewlines) { + if (isHtml) { + lineSep = QStringLiteral("
"); + } else { + lineSep = QLatin1Char('\n'); + } + } + + // Insert the signature and newlines depending on where it was inserted. + int newCursorPos = -1; + QString headSep; + QString tailSep; + + if (placement == KIdentityManagementCore::Signature::End) { + // There is one special case when re-setting the old cursor: The cursor + // was at the end. In this case, QTextEdit has no way to know + // if the signature was added before or after the cursor, and just + // decides that it was added before (and the cursor moves to the end, + // but it should not when appending a signature). See bug 167961 + if (oldCursor.position() == textEdit->toPlainText().length()) { + newCursorPos = oldCursor.position(); + } + headSep = lineSep; + } else if (placement == KIdentityManagementCore::Signature::Start) { + // When prepending signatures, add a couple of new lines before + // the signature, and move the cursor to the beginning of the QTextEdit. + // People tends to insert new text there. + newCursorPos = 0; + headSep = lineSep + lineSep; + if (!isCursorAtEndOfLine(cursor)) { + tailSep = lineSep; + } + } else if (placement == KIdentityManagementCore::Signature::AtCursor) { + if (!isCursorAtEndOfLine(cursor)) { + tailSep = lineSep; + } + } + + const QString full_signature = headSep + signature + tailSep; + if (isHtml) { + textEdit->insertHtml(full_signature); + } else { + textEdit->insertPlainText(full_signature); + } + + cursor.endEditBlock(); + if (newCursorPos != -1) { + oldCursor.setPosition(newCursorPos); + } + + textEdit->setTextCursor(oldCursor); + textEdit->ensureCursorVisible(); + + textEdit->document()->setModified(isModified); + + if (isHtml) { + textEdit->activateRichText(); + } + } +} + +void SignatureRichTextEditor::insertIntoTextEdit(const KIdentityManagementCore::Signature &sig, + KIdentityManagementCore::Signature::Placement placement, + KIdentityManagementCore::Signature::AddedText addedText, + KPIMTextEdit::RichTextComposer *textEdit, + bool forceDisplay) +{ + if (!forceDisplay) { + if (!sig.isEnabledSignature()) { + return; + } + } + QString signature; + if (addedText & KIdentityManagementCore::Signature::AddSeparator) { + signature = sig.withSeparator(); + } else { + signature = sig.rawText(); + } + insertSignatureHelper(signature, + textEdit, + placement, + (sig.isInlinedHtml() && sig.type() == KIdentityManagementCore::Signature::Inlined), + (addedText & KIdentityManagementCore::Signature::AddNewLines)); + + // We added the text of the signature above, now it is time to add the images as well. + if (sig.isInlinedHtml()) { + const auto embeddedImgs = sig.embeddedImages(); + for (const KIdentityManagementCore::Signature::EmbeddedImagePtr &image : embeddedImgs) { + textEdit->composerControler()->composerImages()->loadImage(image->image, image->name, image->name); + } + } +} diff --git a/server/identity/signaturerichtexteditor_p.h b/server/identity/signaturerichtexteditor_p.h new file mode 100644 index 0000000..3aabaa6 --- /dev/null +++ b/server/identity/signaturerichtexteditor_p.h @@ -0,0 +1,50 @@ +/* + SPDX-FileCopyrightText: 2002-2004 Marc Mutz + SPDX-FileCopyrightText: 2007 Tom Albers + SPDX-FileCopyrightText: 2009 Thomas McGuire + Author: Stefan Taferner + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "identity/signature.h" + +namespace KPIMTextEdit +{ +class RichTextComposer; +} + +namespace KIdentityManagementWidgets +{ + +/** Helper methods for rich text signature editing .*/ +namespace SignatureRichTextEditor +{ +/** Inserts this signature into the given text edit. + * If the signature is inserted at the beginning, a couple of new + * lines will be inserted before it, and the cursor is moved to + * the beginning. Otherwise, the cursor position is preserved. + * For undo/redo, this is treated as one operation. + * + * Rich text mode of the text edit will be enabled if the signature is in + * inlined HTML format. + * + * If this signature uses images, they will be added automatically. + * + * @param placement defines where in the text edit the signature should be + * inserted. + * @param addedText defines which other texts should be added to the signature + * @param textEdit the signature will be inserted into this text edit. + * + * @since 4.9 + */ +void insertIntoTextEdit(const KIdentityManagementCore::Signature &sig, + KIdentityManagementCore::Signature::Placement placement, + KIdentityManagementCore::Signature::AddedText addedText, + KPIMTextEdit::RichTextComposer *textEdit, + bool forceDisplay = false); +} + +}