diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..208682d --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: None +# SPDX-License-Identifier: CC0-1.0 + +localhost+1-key.pem +localhost+1.pem \ No newline at end of file diff --git a/README.md b/README.md index 24a8ee7..dd59fe0 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,72 @@ # GPGol.js This project provides a GnuPG integration for users of Outlook Web. ## Build it (Linux) If you distribution provides a sufficient up to date version of the KDE Frameworks 6 and QGpgME, the easiest is to install the development package from your package manager. Otherwise, you can build all the dependencies yourself with [kdesrc-build](https://community.kde.org/Get_Involved/development). Then clone and build gpgol.js: ```sh git clone https://dev.gnupg.org/source/gpgol.js.git cd gpgol.js cmake -GNinja -DCMAKE_EXPORT_COMPILE_COMMANDS=1 -DCMAKE_BUILD_TYPE=Debug -S . build cmake --build build ``` ## Installing the Outlook extension You can install the certificate by visiting the following address: `https://outlook.office365.com/owa/?path=/options/manageapps`. This will load after a short while the addon manager and you can upload the manifest in `broker/manifest.xml` to the user defined Add-Ins. +## Install + +Outlook requires the connections to be using HTTPS, so we need to generate a +self-signed TLS certificate. We use `mkcert` for this: + +```sh +mkcert localhost 127.0.0.1 +mkcert --install + +# copy to server +mkdir -p ~/.local/share/gpgol-server +cp localhost+1-key.pem ~/.local/share/gpgol-server/certificate-key.pem +cp localhost+1.pem ~/.local/share/gpgol-server/certificate.pem + +# copy to client +mkdir -p ~/.local/share/gpgol-client +cp localhost+1.pem ~/.local/share/gpgol-client/certificate.pem +``` + ## Run it The application consists of two process, you need to start both of them: ```sh ./build/bin/gpgol-broker ``` and in a seperate terminal tab ```sh ./build/bin/gpgol-server ``` Outlook requires the connections to be using HTTPS, so you need to trust the self signed certificate in your browser, so open [https://127.0.0.1:5656/home](https://127.0.0.1:5656/home) and trust the certificate. The certificate is also used on port 5657 for the websocket connection, so you might need to trust it also on that port. If you don't want to trust the self signed certificate included in this repo, you can also create your own and replace `broker/assets/certificate.crt` and `broker/assets/private.key`. You will also need to recompile the app. In the future, the certificate should be generated by the installer and trusted automatically when installing the app. diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 59e7517..f62a1bc 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -1,287 +1,279 @@ # SPDX-FileCopyrightText: 2023 g10 code GmbH # SPDX-Contributor: Carl Schwan # SPDX-License-Identifier: BSD-2-Clause add_library(gpgol-client-static STATIC) target_sources(gpgol-client-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/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-client-static - PREFIX - "/" - FILES - assets/certificate.crt -) - - ki18n_wrap_ui(gpgol-client-static editor/attachment/ui/attachmentpropertiesdialog.ui editor/attachment/ui/attachmentpropertiesdialog_readonly.ui ) ecm_qt_declare_logging_category(gpgol-client-static_SRCS HEADER websocket_debug.h IDENTIFIER WEBSOCKET_LOG CATEGORY_NAME org.gpgol.client.websocket DESCRIPTION "Websocket connection in the client" EXPORT GPGOL ) ecm_qt_declare_logging_category(gpgol-client-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-client-static_SRCS HEADER ewscli_debug.h IDENTIFIER EWSCLI_LOG CATEGORY_NAME org.gpgol.ews.client DESCRIPTION "ews client (gpgol-client)" EXPORT GPGOL ) ecm_qt_declare_logging_category(gpgol-client-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-client-static editor/settings/messagecomposersettings.kcfgc) install(FILES composerui.rc DESTINATION ${KDE_INSTALL_KXMLGUIDIR}/gpgol-client) target_sources(gpgol-client-static PUBLIC ${gpgol-client-static_SRCS}) target_link_libraries(gpgol-client-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 ) add_executable(gpgol-client main.cpp) target_link_libraries(gpgol-client PRIVATE gpgol-client-static) if (BUILD_TESTING) add_subdirectory(autotests) endif() diff --git a/client/assets/certificate.crt b/client/assets/certificate.crt deleted file mode 100644 index 67918e3..0000000 --- a/client/assets/certificate.crt +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDezCCAmOgAwIBAgIUHLuHGdIuJmoYvCdmBhZV5Y8KLbMwDQYJKoZIhvcNAQEL -BQAwTTELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVy -bGluMRwwGgYDVQQKDBNEZWZhdWx0IENvbXBhbnkgTHRkMB4XDTIzMTAyNDA3NDQz -NloXDTI0MTAyMzA3NDQzNlowTTELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxp -bjEPMA0GA1UEBwwGQmVybGluMRwwGgYDVQQKDBNEZWZhdWx0IENvbXBhbnkgTHRk -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvs27B7raqGHrjLqGn+wf -Zfbzh80Xl8109/s8KEl+3WtkuDvmJ11v22mvjowA0So6Rh4XhN1pD5K3tOpWwQo7 -oNPtjswBxNFyTU7791xH1P2uGjMvrzp18nJXvDGYvvtseMqiFAmdwcyO4p1CfZq/ -Zqn5/dWcFBMrQvHXOQzF2E3zoMNRXGfGKn8QcqWbgYr3HnIAVYqzdpTVCBzicsnf -+UrZcmv7MJu8F7nNG+fGSsZzaCrr3unFACcWDv/RF8qQalNO7XwrG/w2FLo2GpL7 -KkoK+u1hL9g7O/MHRBIiwllo8YfVNOg8IzQUbtkQ5EEE02yhJbZCQQXJJQwxqk1y -2QIDAQABo1MwUTAdBgNVHQ4EFgQUX6bDEhlgLXy78gaOUoVOeAzxtzQwHwYDVR0j -BBgwFoAUX6bDEhlgLXy78gaOUoVOeAzxtzQwDwYDVR0TAQH/BAUwAwEB/zANBgkq -hkiG9w0BAQsFAAOCAQEAhnKL+NhIfr7I7SGsw1NwJwQ32DtpPC7sFvfqo/JUP7FI -J9BjsV/4U+TIjnZst4EwZ9OfM7haLkRyCIlNSUfPT95Krqr44HFnsneCyMeVj4hP -89zwV0NZGvyaSrQ30iKVUMdeRywwJo6LzWvj8zB+ARN0A8pOi9tBc/xTuW22vqGN -0qeGcqMU0GNK1LW6Pw7zrirXl6ZlgZK1PfIgFzuSq4UAJBQAQ0TfeFItUx30OG1n -P5VNIaEfr4aqnJfrlQ/G6j1dIgJhZx0ea8MaiwHin3hFD33JdI5dNPRbgC77TrbT -W20+bRneoOjG5I/luPyzrcWfd/+w5WjZiR7Dl4ZC2Q== ------END CERTIFICATE----- diff --git a/client/editor/composerviewbase.cpp b/client/editor/composerviewbase.cpp index 358c1aa..eb27423 100644 --- a/client/editor/composerviewbase.cpp +++ b/client/editor/composerviewbase.cpp @@ -1,1352 +1,1355 @@ /* 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 "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) { + + qWarning().noquote() << message->encodedContent(); + auto soapRequestBody = EwsMailFactory::create(message); QNetworkRequest sendMailRequest(QUrl(u"https://127.0.0.1:5656/socket-web"_s)); sendMailRequest.setHeader(QNetworkRequest::ContentTypeHeader, u"application/xml"_s); sendMailRequest.setRawHeader("X-TOKEN", m_bearerToken); sendMailRequest.setRawHeader("X-EMAIL", from().toUtf8()); const QJsonDocument payload(QJsonObject{ { "type"_L1, "ews"_L1 }, { "payload"_L1, soapRequestBody }, { "id"_L1, mailId() }, }); 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/client/editor/util.cpp b/client/editor/util.cpp index 22bda6a..f3164ca 100644 --- a/client/editor/util.cpp +++ b/client/editor/util.cpp @@ -1,426 +1,439 @@ /* SPDX-FileCopyrightText: 2009 Constantin Berzan SPDX-FileCopyrightText: 2009 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.net SPDX-FileCopyrightText: 2009 Leo Franchi Parts based on KMail code by: SPDX-License-Identifier: LGPL-2.0-or-later */ #include "util.h" #include "util_p.h" #include #include #include #include #include "editor_debug.h" #include #include #include #include #include #include #include "job/singlepartjob.h" #include "composer.h" static QString stripOffPrefixes(const QString &subject) { const QStringList replyPrefixes = { QStringLiteral("Re\\s*:"), QStringLiteral("Re\\[\\d+\\]:"), QStringLiteral("Re\\d+:"), }; const QStringList forwardPrefixes = { QStringLiteral("Fwd:"), QStringLiteral("FW:<"), }; const QStringList prefixRegExps = replyPrefixes + forwardPrefixes; // construct a big regexp that // 1. is anchored to the beginning of str (sans whitespace) // 2. matches at least one of the part regexps in prefixRegExps const QString bigRegExp = QStringLiteral("^(?:\\s+|(?:%1))+\\s*").arg(prefixRegExps.join(QStringLiteral(")|(?:"))); static QRegularExpression regex; if (regex.pattern() != bigRegExp) { // the prefixes have changed, so update the regexp regex.setPattern(bigRegExp); regex.setPatternOptions(QRegularExpression::CaseInsensitiveOption); } if (regex.isValid()) { QRegularExpressionMatch match = regex.match(subject); if (match.hasMatch()) { return subject.mid(match.capturedEnd(0)); } } else { qCWarning(EDITOR_LOG) << "bigRegExp = \"" << bigRegExp << "\"\n" << "prefix regexp is invalid!"; } return subject; } KMime::Content *setBodyAndCTE(QByteArray &encodedBody, KMime::Headers::ContentType *contentType, KMime::Content *ret) { MessageComposer::Composer composer; MessageComposer::SinglepartJob cteJob(&composer); cteJob.contentType()->setMimeType(contentType->mimeType()); cteJob.contentType()->setCharset(contentType->charset()); cteJob.setData(encodedBody); cteJob.exec(); cteJob.content()->assemble(); ret->contentTransferEncoding()->setEncoding(cteJob.contentTransferEncoding()->encoding()); ret->setBody(cteJob.content()->encodedBody()); return ret; } KMime::Content *MessageComposer::Util::composeHeadersAndBody(KMime::Content *orig, QByteArray encodedBody, Kleo::CryptoMessageFormat format, bool sign, const QByteArray &hashAlgo) { auto result = new KMime::Content; // called should have tested that the signing/encryption failed Q_ASSERT(!encodedBody.isEmpty()); if (!(format & Kleo::InlineOpenPGPFormat)) { // make a MIME message qCDebug(EDITOR_LOG) << "making MIME message, format:" << format; makeToplevelContentType(result, format, sign, hashAlgo); if (makeMultiMime(format, sign)) { // sign/enc PGPMime, sign SMIME const QByteArray boundary = KMime::multiPartBoundary(); result->contentType()->setBoundary(boundary); result->assemble(); // qCDebug(EDITOR_LOG) << "processed header:" << result->head(); // Build the encapsulated MIME parts. // Build a MIME part holding the code information // taking the body contents returned in ciphertext. auto code = new KMime::Content; setNestedContentType(code, format, sign); setNestedContentDisposition(code, format, sign); if (sign) { // sign PGPMime, sign SMIME if (format & Kleo::AnySMIME) { // sign SMIME auto ct = code->contentTransferEncoding(); // create ct->setEncoding(KMime::Headers::CEbase64); ct->needToEncode(); code->setBody(encodedBody); } else { // sign PGPMmime setBodyAndCTE(encodedBody, orig->contentType(), code); } result->appendContent(orig); result->appendContent(code); } else { // enc PGPMime setBodyAndCTE(encodedBody, orig->contentType(), code); + auto header = new KMime::Headers::Generic("PGP"); + header->fromUnicodeString(QStringLiteral("encrypted"), "utf-8"); + result->setHeader(header); + auto instruction = new KMime::Content; instruction->contentType()->setMimeType("text/plain"); instruction->contentTransferEncoding()->setEncoding(KMime::Headers::CE7Bit); if (Kleo::DeVSCompliance::isCompliant()) { instruction->setBody("This message is VS-NfD compliant encrypted and need to be opened in a compliant E-Mail client. For example: GnuPG VS-DesktopĀ®"); } else { instruction->setBody("This message is encrypted with OpenPGP and need to be opened in a compatible client. For example: GPG4Win, KMail, Kleopatra or Thunderbird"); } + auto encryptedPart = new KMime::Content; + auto ct = encryptedPart->contentType(); // Create + ct->setMimeType(QByteArrayLiteral("multipart/encrypted")); + ct->setParameter(QStringLiteral("protocol"), QStringLiteral("application/pgp-encrypted")); + const QByteArray boundary = KMime::multiPartBoundary(); + ct->setBoundary(boundary); + // Build a MIME part holding the version information // taking the body contents returned in // structuring.data.bodyTextVersion. auto vers = new KMime::Content; vers->contentType()->setMimeType("application/pgp-encrypted"); vers->contentDisposition()->setDisposition(KMime::Headers::CDattachment); vers->contentTransferEncoding()->setEncoding(KMime::Headers::CE7Bit); vers->setBody("Version: 1"); + encryptedPart->appendContent(vers); + encryptedPart->appendContent(code); + result->appendContent(instruction); - result->appendContent(vers); - result->appendContent(code); + result->appendContent(encryptedPart); } } else { // enc SMIME, sign/enc SMIMEOpaque const QByteArray boundary = KMime::multiPartBoundary(); result->contentType()->setBoundary(boundary); result->assemble(); // qCDebug(EDITOR_LOG) << "processed header:" << result->head(); // Build the encapsulated MIME parts. // Build a MIME part holding the code information // taking the body contents returned in ciphertext. auto code = new KMime::Content; setNestedContentType(code, format, sign); setNestedContentDisposition(code, format, sign); setBodyAndCTE(encodedBody, orig->contentType(), code); code->assemble(); code->setBody(encodedBody); auto instruction = new KMime::Content; instruction->contentType()->setMimeType("text/plain"); instruction->contentTransferEncoding()->setEncoding(KMime::Headers::CE7Bit); if (Kleo::DeVSCompliance::isCompliant()) { instruction->setBody("This message is VS-NfD compliant encrypted and need to be opened in a compliant E-Mail client. For example: GnuPG VS-DesktopĀ®"); } else { instruction->setBody("This message is encrypted with S/MIME and need to be opened in a compatible client. For example: GPG4Win, KMail, Kleopatra or Thunderbird"); } result->appendContent(instruction); result->appendContent(code); } } else { // sign/enc PGPInline result->setHead(orig->head()); result->parse(); // fixing ContentTransferEncoding setBodyAndCTE(encodedBody, orig->contentType(), result); } return result; } // set the correct top-level ContentType on the message void MessageComposer::Util::makeToplevelContentType(KMime::Content *content, Kleo::CryptoMessageFormat format, bool sign, const QByteArray &hashAlgo) { switch (format) { default: case Kleo::InlineOpenPGPFormat: case Kleo::OpenPGPMIMEFormat: { auto ct = content->contentType(); // Create if (sign) { ct->setMimeType(QByteArrayLiteral("multipart/signed")); ct->setParameter(QStringLiteral("protocol"), QStringLiteral("application/pgp-signature")); ct->setParameter(QStringLiteral("micalg"), QString::fromLatin1(QByteArray(QByteArrayLiteral("pgp-") + hashAlgo)).toLower()); } else { ct->setMimeType(QByteArrayLiteral("multipart/encrypted")); ct->setParameter(QStringLiteral("protocol"), QStringLiteral("application/pgp-encrypted")); } } return; case Kleo::SMIMEOpaqueFormat: case Kleo::SMIMEFormat: { auto ct = content->contentType(); // Create if (sign) { qCDebug(EDITOR_LOG) << "setting headers for SMIME"; ct->setMimeType(QByteArrayLiteral("multipart/signed")); ct->setParameter(QStringLiteral("protocol"), QStringLiteral("application/pkcs7-signature")); ct->setParameter(QStringLiteral("micalg"), QString::fromLatin1(hashAlgo).toLower()); return; } else { ct->setMimeType(QByteArrayLiteral("multipart/encrypted")); ct->setParameter(QStringLiteral("protocol"), QStringLiteral("application/smime-encrypted")); } // fall through (for encryption, there's no difference between // SMIME and SMIMEOpaque, since there is no mp/encrypted for // S/MIME) } } } void MessageComposer::Util::setNestedContentType(KMime::Content *content, Kleo::CryptoMessageFormat format, bool sign) { switch (format) { case Kleo::OpenPGPMIMEFormat: { auto ct = content->contentType(); // Create if (sign) { ct->setMimeType(QByteArrayLiteral("application/pgp-signature")); ct->setParameter(QStringLiteral("name"), QStringLiteral("signature.asc")); content->contentDescription()->from7BitString("This is a digitally signed message part."); } else { ct->setMimeType(QByteArrayLiteral("application/octet-stream")); } } return; case Kleo::SMIMEFormat: { auto ct = content->contentType(); // Create if (sign) { ct->setMimeType(QByteArrayLiteral("application/pkcs7-signature")); ct->setParameter(QStringLiteral("name"), QStringLiteral("smime.p7s")); ct->setParameter(QStringLiteral("smime-type"), QStringLiteral("signed-data")); } else { auto ct = content->contentType(); // Create ct->setMimeType(QByteArrayLiteral("application/pkcs7-mime")); ct->setParameter(QStringLiteral("name"), QStringLiteral("smime.p7m")); ct->setParameter(QStringLiteral("smime-type"), QStringLiteral("enveloped-data")); } return; } default: case Kleo::InlineOpenPGPFormat: case Kleo::SMIMEOpaqueFormat:; } } void MessageComposer::Util::setNestedContentDisposition(KMime::Content *content, Kleo::CryptoMessageFormat format, bool sign) { auto ct = content->contentDisposition(); if (!sign && format & Kleo::OpenPGPMIMEFormat) { ct->setDisposition(KMime::Headers::CDinline); ct->setFilename(QStringLiteral("msg.asc")); } else if (sign && format & Kleo::SMIMEFormat) { ct->setDisposition(KMime::Headers::CDattachment); ct->setFilename(QStringLiteral("smime.p7s")); } } bool MessageComposer::Util::makeMultiMime(Kleo::CryptoMessageFormat format, bool sign) { switch (format) { default: case Kleo::InlineOpenPGPFormat: case Kleo::SMIMEOpaqueFormat: return false; case Kleo::OpenPGPMIMEFormat: return true; case Kleo::SMIMEFormat: return sign; // only on sign - there's no mp/encrypted for S/MIME } } QByteArray MessageComposer::Util::selectCharset(const QList &charsets, const QString &text) { for (const QByteArray &name : charsets) { // We use KCharsets::codecForName() instead of QTextCodec::codecForName() here, because // the former knows us-ascii is latin1. QStringEncoder codec(name.constData()); if (!codec.isValid()) { qCWarning(EDITOR_LOG) << "Could not get text codec for charset" << name; continue; } if (codec.encode(text); !codec.hasError()) { // Special check for us-ascii (needed because us-ascii is not exactly latin1). if (name == "us-ascii" && !KMime::isUsAscii(text)) { continue; } qCDebug(EDITOR_LOG) << "Chosen charset" << name; return name; } } qCDebug(EDITOR_LOG) << "No appropriate charset found."; return {}; } QStringList MessageComposer::Util::AttachmentKeywords() { return i18nc( "comma-separated list of keywords that are used to detect whether " "the user forgot to attach his attachment. Do not add space between words.", "attachment,attached") .split(QLatin1Char(',')); } QString MessageComposer::Util::cleanedUpHeaderString(const QString &s) { // remove invalid characters from the header strings QString res(s); res.remove(QChar::fromLatin1('\r')); res.replace(QChar::fromLatin1('\n'), QLatin1Char(' ')); return res.trimmed(); } KMime::Content *MessageComposer::Util::findTypeInMessage(KMime::Content *data, const QByteArray &mimeType, const QByteArray &subType) { if (!data->contentType()->isEmpty()) { if (mimeType.isEmpty() || subType.isEmpty()) { return data; } if ((mimeType == data->contentType()->mediaType()) && (subType == data->contentType(false)->subType())) { return data; } } const auto contents = data->contents(); for (auto child : contents) { if ((!child->contentType()->isEmpty()) && (mimeType == child->contentType()->mimeType()) && (subType == child->contentType()->subType())) { return child; } auto ret = findTypeInMessage(child, mimeType, subType); if (ret) { return ret; } } return nullptr; } bool MessageComposer::Util::hasMissingAttachments(const QStringList &attachmentKeywords, QTextDocument *doc, const QString &subj) { if (!doc) { return false; } QStringList attachWordsList = attachmentKeywords; QRegularExpression rx(QLatin1String("\\b") + attachWordsList.join(QLatin1String("\\b|\\b")) + QLatin1String("\\b"), QRegularExpression::CaseInsensitiveOption); // check whether the subject contains one of the attachment key words // unless the message is a reply or a forwarded message bool gotMatch = (stripOffPrefixes(subj) == subj) && (rx.match(subj).hasMatch()); if (!gotMatch) { // check whether the non-quoted text contains one of the attachment key // words static QRegularExpression quotationRx(QStringLiteral("^([ \\t]*([|>:}#]|[A-Za-z]+>))+")); QTextBlock end(doc->end()); for (QTextBlock it = doc->begin(); it != end; it = it.next()) { const QString line = it.text(); gotMatch = (!quotationRx.match(line).hasMatch()) && (rx.match(line).hasMatch()); if (gotMatch) { break; } } } if (!gotMatch) { return false; } return true; } static QStringList encodeIdn(const QStringList &emails) { QStringList encoded; encoded.reserve(emails.count()); for (const QString &email : emails) { encoded << KEmailAddress::normalizeAddressesAndEncodeIdn(email); } return encoded; } QStringList MessageComposer::Util::cleanEmailList(const QStringList &emails) { QStringList clean; clean.reserve(emails.count()); for (const QString &email : emails) { clean << KEmailAddress::extractEmailAddress(email); } return clean; } QStringList MessageComposer::Util::cleanUpEmailListAndEncoding(const QStringList &emails) { return cleanEmailList(encodeIdn(emails)); } void MessageComposer::Util::addCustomHeaders(const KMime::Message::Ptr &message, const QMap &custom) { QMapIterator customHeader(custom); while (customHeader.hasNext()) { customHeader.next(); auto header = new KMime::Headers::Generic(customHeader.key().constData()); header->fromUnicodeString(customHeader.value(), "utf-8"); message->setHeader(header); } } diff --git a/client/websocketclient.cpp b/client/websocketclient.cpp index 1ea0dfb..012f3da 100644 --- a/client/websocketclient.cpp +++ b/client/websocketclient.cpp @@ -1,186 +1,190 @@ // SPDX-FileCopyrightText: 2023 g10 code Gmbh // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "websocketclient.h" // Qt headers #include #include #include #include #include #include #include +#include // KDE headers #include #include // gpgme headers #include #include "websocket_debug.h" #include "qnam.h" using namespace std::chrono; using namespace Qt::Literals::StringLiterals; namespace { QStringList trustedEmails(const std::shared_ptr &keyCache) { QStringList emails; const auto keys = keyCache->keys(); for (const auto &key : keys) { for (const auto &userId : key.userIDs()) { if (key.ownerTrust() == GpgME::Key::Ultimate) { emails << QString::fromLatin1(userId.email()).toLower(); break; } } } return emails; } auto delay = 2000ms; void registerServer(int port, const QStringList &emails, const QString &serverId) { QNetworkRequest registerRequest(QUrl(u"https://127.0.0.1:5656/register"_s)); registerRequest.setHeader(QNetworkRequest::ContentTypeHeader, u"application/json"_s); auto registerReply = qnam->post(registerRequest, QJsonDocument(QJsonObject{ { "port"_L1, port }, { "emails"_L1, QJsonArray::fromStringList(emails) }, { "serverId"_L1, serverId }, }).toJson()); QObject::connect(registerReply, &QNetworkReply::finished, qnam, [port, emails, serverId, registerReply]() { if (registerReply->error() != QNetworkReply::NoError) { QTimer::singleShot(delay, [emails, port, serverId]() { delay *= 2; registerServer(port, emails, serverId); }); qWarning() << "Failed to register" << registerReply->errorString() << "retrying in" << delay; } else { qWarning() << "Register"; } registerReply->deleteLater(); }); } } WebsocketClient &WebsocketClient::self(const QUrl &url, int port) { static WebsocketClient *client = nullptr; if (!client && url.isEmpty()) { qFatal() << "Unable to create a client without an url"; } else if (!client) { client = new WebsocketClient(url, port); } return *client; }; WebsocketClient::WebsocketClient(const QUrl &url, int port) : QObject(nullptr) , m_url(url) , m_port(port) { connect(&m_webSocket, &QWebSocket::connected, this, &WebsocketClient::slotConnected); connect(&m_webSocket, &QWebSocket::disconnected, this, [this] { Q_EMIT closed(i18nc("@info", "Connection to outlook lost due to a disconnection with the broker.")); }); connect(&m_webSocket, &QWebSocket::errorOccurred, this, &WebsocketClient::slotErrorOccurred); connect(&m_webSocket, &QWebSocket::textMessageReceived, this, &WebsocketClient::slotTextMessageReceived); connect(&m_webSocket, QOverload&>::of(&QWebSocket::sslErrors), this, [this](const QList &errors) { // TODO remove m_webSocket.ignoreSslErrors(errors); }); QSslConfiguration sslConfiguration; - QFile certFile(QStringLiteral(":/assets/certificate.crt")); + auto certPath = QStandardPaths::locate(QStandardPaths::AppLocalDataLocation, QStringLiteral("certificate.pem")); + Q_ASSERT(!certPath.isEmpty()); + + QFile certFile(certPath); if (!certFile.open(QIODevice::ReadOnly)) { qFatal() << "Couldn't read certificate"; } QSslCertificate certificate(&certFile, QSsl::Pem); certFile.close(); sslConfiguration.addCaCertificate(certificate); m_webSocket.setSslConfiguration(sslConfiguration); m_webSocket.open(url); auto globalKeyCache = Kleo::KeyCache::instance(); m_emails = trustedEmails(globalKeyCache); if (m_emails.isEmpty()) { qWarning() << "No ultimately keys found in keychain"; return; } m_serverId = QUuid::createUuid().toString(QUuid::WithoutBraces); // TODO remove me QObject::connect(qnam, &QNetworkAccessManager::sslErrors, qnam, [](QNetworkReply *reply, const QList &errors) { reply->ignoreSslErrors(); }); registerServer(m_port, m_emails, m_serverId); } void WebsocketClient::slotConnected() { qCInfo(WEBSOCKET_LOG) << "websocket connected"; QJsonDocument doc(QJsonObject{ { "command"_L1, "register"_L1 }, { "arguments"_L1, QJsonObject { { "emails"_L1, QJsonArray::fromStringList(m_emails) }, { "type"_L1, "nativeclient"_L1 } }} }); m_webSocket.sendTextMessage(QString::fromUtf8(doc.toJson())); registerServer(m_port, m_emails, m_serverId); Q_EMIT connected(); } void WebsocketClient::slotErrorOccurred(QAbstractSocket::SocketError error) { qCWarning(WEBSOCKET_LOG) << error << m_webSocket.errorString(); Q_EMIT closed(i18nc("@info", "Connection to outlook lost due to a connection error.")); reconnect(); } void WebsocketClient::slotTextMessageReceived(QString message) { const auto doc = QJsonDocument::fromJson(message.toUtf8()); if (!doc.isObject()) { qCWarning(WEBSOCKET_LOG) << "invalid text message received" << message; return; } const auto object = doc.object(); qCDebug(WEBSOCKET_LOG) << object; if (object["type"_L1] == "disconnection"_L1) { // disconnection of the web client Q_EMIT closed(i18nc("@info", "Connection to outlook lost. Make sure the extension tab is open.")); } else if (object["type"_L1] == "connection"_L1) { // reconnection of the web client Q_EMIT connected(); } else if (object["type"_L1] == "email-sent"_L1) { // confirmation that the email was sent const auto args = object["arguments"_L1].toObject(); Q_EMIT emailSentSuccessfully(args["id"_L1].toString()); } } void WebsocketClient::reconnect() { QTimer::singleShot(1000ms, this, [this]() { m_webSocket.open(m_url); }); } diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 265e642..0fda7e6 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -1,45 +1,42 @@ # SPDX-FileCopyrightText: 2023 g10 code GmbH # SPDX-Contributor: Carl Schwan # SPDX-License-Identifier: BSD-2-Clause add_executable(gpgol-server) target_sources(gpgol-server PRIVATE # Controllers controllers/abstractcontroller.cpp controllers/abstractcontroller.h controllers/registrationcontroller.cpp controllers/registrationcontroller.h controllers/staticcontroller.h controllers/staticcontroller.cpp controllers/emailcontroller.cpp controllers/emailcontroller.h # State model/serverstate.cpp model/serverstate.h # web sever webserver.cpp webserver.h main.cpp ) qt_add_resources(gpgol-server PREFIX "/" FILES - assets/certificate.crt - assets/private.key - assets/document-decrypt-16.png assets/document-decrypt-32.png assets/document-decrypt-64.png assets/document-decrypt-80.png assets/script.js web/index.html ) target_link_libraries(gpgol-server PRIVATE Qt6::HttpServer Qt6::Core common) diff --git a/server/assets/certificate.crt b/server/assets/certificate.crt deleted file mode 100644 index 67918e3..0000000 --- a/server/assets/certificate.crt +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDezCCAmOgAwIBAgIUHLuHGdIuJmoYvCdmBhZV5Y8KLbMwDQYJKoZIhvcNAQEL -BQAwTTELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVy -bGluMRwwGgYDVQQKDBNEZWZhdWx0IENvbXBhbnkgTHRkMB4XDTIzMTAyNDA3NDQz -NloXDTI0MTAyMzA3NDQzNlowTTELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxp -bjEPMA0GA1UEBwwGQmVybGluMRwwGgYDVQQKDBNEZWZhdWx0IENvbXBhbnkgTHRk -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvs27B7raqGHrjLqGn+wf -Zfbzh80Xl8109/s8KEl+3WtkuDvmJ11v22mvjowA0So6Rh4XhN1pD5K3tOpWwQo7 -oNPtjswBxNFyTU7791xH1P2uGjMvrzp18nJXvDGYvvtseMqiFAmdwcyO4p1CfZq/ -Zqn5/dWcFBMrQvHXOQzF2E3zoMNRXGfGKn8QcqWbgYr3HnIAVYqzdpTVCBzicsnf -+UrZcmv7MJu8F7nNG+fGSsZzaCrr3unFACcWDv/RF8qQalNO7XwrG/w2FLo2GpL7 -KkoK+u1hL9g7O/MHRBIiwllo8YfVNOg8IzQUbtkQ5EEE02yhJbZCQQXJJQwxqk1y -2QIDAQABo1MwUTAdBgNVHQ4EFgQUX6bDEhlgLXy78gaOUoVOeAzxtzQwHwYDVR0j -BBgwFoAUX6bDEhlgLXy78gaOUoVOeAzxtzQwDwYDVR0TAQH/BAUwAwEB/zANBgkq -hkiG9w0BAQsFAAOCAQEAhnKL+NhIfr7I7SGsw1NwJwQ32DtpPC7sFvfqo/JUP7FI -J9BjsV/4U+TIjnZst4EwZ9OfM7haLkRyCIlNSUfPT95Krqr44HFnsneCyMeVj4hP -89zwV0NZGvyaSrQ30iKVUMdeRywwJo6LzWvj8zB+ARN0A8pOi9tBc/xTuW22vqGN -0qeGcqMU0GNK1LW6Pw7zrirXl6ZlgZK1PfIgFzuSq4UAJBQAQ0TfeFItUx30OG1n -P5VNIaEfr4aqnJfrlQ/G6j1dIgJhZx0ea8MaiwHin3hFD33JdI5dNPRbgC77TrbT -W20+bRneoOjG5I/luPyzrcWfd/+w5WjZiR7Dl4ZC2Q== ------END CERTIFICATE----- diff --git a/server/assets/private.key b/server/assets/private.key deleted file mode 100644 index bdfad76..0000000 --- a/server/assets/private.key +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC+zbsHutqoYeuM -uoaf7B9l9vOHzReXzXT3+zwoSX7da2S4O+YnXW/baa+OjADRKjpGHheE3WkPkre0 -6lbBCjug0+2OzAHE0XJNTvv3XEfU/a4aMy+vOnXycle8MZi++2x4yqIUCZ3BzI7i -nUJ9mr9mqfn91ZwUEytC8dc5DMXYTfOgw1FcZ8YqfxBypZuBivcecgBVirN2lNUI -HOJyyd/5Stlya/swm7wXuc0b58ZKxnNoKuve6cUAJxYO/9EXypBqU07tfCsb/DYU -ujYakvsqSgr67WEv2Ds78wdEEiLCWWjxh9U06DwjNBRu2RDkQQTTbKEltkJBBckl -DDGqTXLZAgMBAAECggEADfI/KrI5c8fOfgePOga6B8usYr92q0GY/Fw4ejBfDcOh -BcdZDAUMHUNFEfwBPkiiIEhoSy2c1YbRly9uvmbnU1rSORcHxRFTE7wDdkHgPTz6 -mg8mO95fyyQY7hhI9CKv+8dB4AmQ5p6zTfSVLNWgClq/u1I++YeqKJdBNjoNBwGR -SkH/0GS2wL2gJs5+IRWJ8nAzSGmUhsAskzh7A+ODLG31Jfh1kCb0CZqMVN6e/kWi -I6YuUOmLHkGX+3sVnsDC2KRJFJ9+q5l7Tnx8BB46fuuDbWS+zm3U4s+bY/T4Ii2N -+iMPJ5GBpAR11mxQoAbx02ONPTUUMTK4GHJm+0nLgQKBgQDk6ZyxBkKNhQD+SeVs -ca8CK2WgJgOtQ28377wzZlbic6e0/mSAD8CuUfPUOyeJ4uHUhrYAuIPWbmwBmKo2 -kWQ56eQOx2S+HGIdhKuVkLpAQBkZNg2XNikl9hjPOnB2APG31/jUWP3u4xUfJRkt -uR8RkGiQ9XfiS9l9HYzLCpPFQQKBgQDVYbH44dB0YjBb2/JvnZrWExedCV0n4JGJ -FjytlgHS5FDguDBINKREBsELrjX2YD585zzwpkdcjM2Emyb7yNkE1ZZRbcQzegCh -FOzCGDB5wt7uwSoCUgru0FWGeTnHakV3o7sM9PG0pVkLO3IZoluU8hyE+MtHnQ1W -pFmHGz3PmQKBgHbhR6m7mYaLYzesQsRtybhztyRTeadalQDUtMVwyYBzFc5uKt1C -0Qr2ro0yZ1c5h5RqnUyEhpsu31J46TTLjuRwripqrMv6S44O31keP0usvhwUsTwt -OXNoefvs2oi99spGFAibaszMC6UQDCJtuE/T0iouAKN6RqmuVfcAODvBAoGAAXY+ -4uahJ/r4O9jg61TYF7lfwwqqYbiPJrJJTiMl9gzsZO2+HvCFY5XsQqk542s7Yl2v -NDLfcEniqpLpjHYOzpKsCfjHjFa8pF3hVV8XDe6A2n/mSsfHHWW+AELaW3xl53Du -uiL/eO5dEs+gc36CRB34hgyyT9ZKnhpYJq3QGZkCgYEApLmJpSjm+86Z5WSdQR4Y -vHuoYX49BmsjYr5NPFQBMspB5CDHRpUhh4ycKRXzWEpP8rK8pR3zirctYpcuxaVu -GhyZjFSFDh/iQcUmeCIK7LgvLjZ+zdzxEQnGBhVs2nSjIXu7Tm/OJQ8vIA12NCmK -1UAOjtpzX6LHFjhCw1mEVsE= ------END PRIVATE KEY----- diff --git a/server/webserver.cpp b/server/webserver.cpp index 4e7da5a..52d7935 100644 --- a/server/webserver.cpp +++ b/server/webserver.cpp @@ -1,300 +1,305 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "webserver.h" #include #include #include #include #include #include #include #include #include #include #include #include +#include #include "controllers/registrationcontroller.h" #include "controllers/staticcontroller.h" #include "controllers/emailcontroller.h" using namespace Qt::Literals::StringLiterals; WebServer WebServer::s_instance = WebServer(); WebServer &WebServer::self() { return s_instance; } WebServer::WebServer() : QObject(nullptr) , m_httpServer(new QHttpServer(this)) , m_webSocketServer(new QWebSocketServer(u"GPGOL"_s, QWebSocketServer::SslMode::SecureMode, this)) { } WebServer::~WebServer() = default; bool WebServer::run() { - QFile privateKeyFile(QStringLiteral(":/assets/private.key")); + auto keyPath = QStandardPaths::locate(QStandardPaths::AppLocalDataLocation, QStringLiteral("certificate-key.pem")); + auto certPath = QStandardPaths::locate(QStandardPaths::AppLocalDataLocation, QStringLiteral("certificate.pem")); + Q_ASSERT(!keyPath.isEmpty()); + Q_ASSERT(!certPath.isEmpty()); + + QFile privateKeyFile(keyPath); if (!privateKeyFile.open(QIODevice::ReadOnly)) { qWarning() << u"Couldn't open file for reading: %1"_s.arg(privateKeyFile.errorString()); return false; } const QSslKey sslKey(&privateKeyFile, QSsl::Rsa); privateKeyFile.close(); - const auto sslCertificateChain = - QSslCertificate::fromPath(QStringLiteral(":/assets/certificate.crt")); + const auto sslCertificateChain = QSslCertificate::fromPath(certPath); if (sslCertificateChain.isEmpty()) { qWarning() << u"Couldn't retrieve SSL certificate from file."_s; return false; } // Static assets controller m_httpServer->route(u"/home"_s, &StaticController::homeAction); m_httpServer->route(u"/assets/"_s, &StaticController::assetsAction); // Registration controller m_httpServer->route(u"/register"_s, &RegistrationController::registerAction); // Email controller m_httpServer->route(u"/view"_s, &EmailController::viewEmailAction); m_httpServer->route(u"/info"_s, &EmailController::infoEmailAction); m_httpServer->route(u"/reply"_s, &EmailController::replyEmailAction); m_httpServer->route(u"/forward"_s, &EmailController::forwardEmailAction); m_httpServer->route(u"/new"_s, &EmailController::newEmailAction); m_httpServer->route(u"/socket-web"_s, &EmailController::socketWebAction); m_httpServer->route(u"/draft/"_s, &EmailController::draftAction); m_httpServer->afterRequest([](QHttpServerResponse &&resp) { resp.setHeader("Access-Control-Allow-Origin", "*"); return std::move(resp); }); m_httpServer->sslSetup(sslCertificateChain.front(), sslKey); const auto port = m_httpServer->listen(QHostAddress::Any, WebServer::Port); if (!port) { qWarning() << "Server failed to listen on a port."; return false; } qWarning() << u"Running http server on https://127.0.0.1:%1/ (Press CTRL+C to quit)"_s.arg(port); QSslConfiguration sslConfiguration; sslConfiguration.setPeerVerifyMode(QSslSocket::VerifyNone); sslConfiguration.setLocalCertificate(sslCertificateChain.front()); sslConfiguration.setPrivateKey(sslKey); m_webSocketServer->setSslConfiguration(sslConfiguration); if (m_webSocketServer->listen(QHostAddress::Any, WebServer::WebSocketPort)) { qWarning() << u"Running websocket server on wss://127.0.0.1:%1/ (Press CTRL+C to quit)"_s.arg(WebServer::Port + 1); connect(m_webSocketServer, &QWebSocketServer::newConnection, this, &WebServer::onNewConnection); } return true; } void WebServer::onNewConnection() { auto pSocket = m_webSocketServer->nextPendingConnection(); if (!pSocket) { return; } qDebug() << "Client connected:" << pSocket->peerName() << pSocket->origin(); connect(pSocket, &QWebSocket::textMessageReceived, this, &WebServer::processTextMessage); connect(pSocket, &QWebSocket::binaryMessageReceived, this, &WebServer::processBinaryMessage); connect(pSocket, &QWebSocket::disconnected, this, &WebServer::socketDisconnected); m_clients << pSocket; } void WebServer::processTextMessage(QString message) { auto webClient = qobject_cast(sender()); if (webClient) { QJsonParseError error; const auto doc = QJsonDocument::fromJson(message.toUtf8(), &error); if (error.error != QJsonParseError::NoError) { qWarning() << "Error parsing json" << error.errorString(); return; } if (!doc.isObject()) { qWarning() << "Invalid json received"; return; } const auto object = doc.object(); if (!object.contains("command"_L1) || !object["command"_L1].isString() || !object.contains("arguments"_L1) || !object["arguments"_L1].isObject()) { qWarning() << "Invalid json received: no command or arguments set" ; return; } static QHash commandMapping { { "register"_L1, WebServer::Command::Register }, { "email-sent"_L1, WebServer::Command::EmailSent }, }; const auto command = commandMapping[doc["command"_L1].toString()]; processCommand(command, object["arguments"_L1].toObject(), webClient); } } void WebServer::processCommand(Command command, const QJsonObject &arguments, QWebSocket *socket) { switch (command) { case Command::Register: { const auto type = arguments["type"_L1].toString(); qDebug() << "Register" << arguments; if (type.isEmpty()) { qWarning() << "empty client type given when registering"; return; } const auto emails = arguments["emails"_L1].toArray(); if (type == "webclient"_L1) { if (emails.isEmpty()) { qWarning() << "empty email given"; } for (const auto &email : emails) { m_webClientsMappingToEmail[email.toString()] = socket; qWarning() << "email" << email.toString() << "mapped to a web client"; const auto nativeClient = m_nativeClientsMappingToEmail[email.toString()]; if (nativeClient) { QJsonDocument doc(QJsonObject{ { "type"_L1, "connection"_L1 }, { "payload"_L1, QJsonObject{ { "client_type"_L1, "web_client"_L1 } }} }); nativeClient->sendTextMessage(QString::fromUtf8(doc.toJson())); } } } else { if (emails.isEmpty()) { qWarning() << "empty email given"; } for (const auto &email : emails) { m_nativeClientsMappingToEmail[email.toString()] = socket; qWarning() << "email" << email.toString() << "mapped to a native client"; const auto webClient = m_webClientsMappingToEmail[email.toString()]; if (webClient) { QJsonDocument doc(QJsonObject{ { "type"_L1, "connection"_L1 }, { "payload"_L1, QJsonObject{ { "client_type"_L1, "native_client"_L1 } }} }); webClient->sendTextMessage(QString::fromUtf8(doc.toJson())); } } } return; } case Command::EmailSent: { const auto email = arguments["email"_L1].toString(); const auto socket = m_nativeClientsMappingToEmail[email]; if (!socket) { return; } QJsonDocument doc(QJsonObject{ { "type"_L1, "email-sent"_L1 }, { "arguments"_L1, arguments }, }); socket->sendTextMessage(QString::fromUtf8(doc.toJson())); return; } case Command::Undefined: qWarning() << "Invalid json received: invalid command" ; return; } } bool WebServer::sendMessageToWebClient(const QString &email, const QByteArray &payload) { auto socket = m_webClientsMappingToEmail[email]; if (!socket) { return false; } socket->sendTextMessage(QString::fromUtf8(payload)); return true; } void WebServer::processBinaryMessage(QByteArray message) { qWarning() << "got binary message" << message; QWebSocket *pClient = qobject_cast(sender()); if (pClient) { pClient->sendBinaryMessage(message); } } void WebServer::socketDisconnected() { QWebSocket *pClient = qobject_cast(sender()); if (pClient) { qDebug() << "Client disconnected" << pClient; // Web client was disconnected { const auto it = std::find_if(m_webClientsMappingToEmail.cbegin(), m_webClientsMappingToEmail.cend(), [pClient](QWebSocket *webSocket) { return pClient == webSocket; }); if (it != m_webClientsMappingToEmail.cend()) { const auto email = it.key(); const auto nativeClient = m_nativeClientsMappingToEmail[email]; qDebug() << "webclient with email disconnected" << email << nativeClient; if (nativeClient) { QJsonDocument doc(QJsonObject{ { "type"_L1, "disconnection"_L1 }, }); nativeClient->sendTextMessage(QString::fromUtf8(doc.toJson())); } m_webClientsMappingToEmail.removeIf([pClient](auto socket) { return pClient == socket.value(); }); } } // Native client was disconnected const auto emails = m_nativeClientsMappingToEmail.keys(); for (const auto &email : emails) { const auto webSocket = m_nativeClientsMappingToEmail[email]; if (webSocket != pClient) { qDebug() << "webSocket not equal" << email << webSocket << pClient; continue; } qDebug() << "native client for" << email << "was disconnected."; QJsonDocument doc(QJsonObject{ { "type"_L1, "disconnection"_L1 }, }); sendMessageToWebClient(email, doc.toJson()); } m_nativeClientsMappingToEmail.removeIf([pClient](auto socket) { return pClient == socket.value(); }); m_clients.removeAll(pClient); } }