diff --git a/CMakeLists.txt b/CMakeLists.txt index d63b227..c72dcf7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,57 +1,62 @@ # SPDX-FileCopyrightText: 2023 g10 code Gmbh # SPDX-Contributor: Carl Schwan # SPDX-License-Identifier: BSD-2-Clause cmake_minimum_required(VERSION 3.16) project(gpgoljs) set(KF_MIN_VERSION "5.240.0") find_package(ECM ${KF_MIN_VERSION} CONFIG REQUIRED) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH} ) find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake) include(FeatureSummary) include(KDEInstallDirs) include(KDECMakeSettings) include(ECMQtDeclareLoggingCategory) +include(ECMAddTests) include(KDECompilerSettings NO_POLICY_SCOPE) set(QT_MIN_VERSION "6.5") set(KF_MIN_VERSION "5.240") set(MIMETREEPARSER_VERSION "5.240.40") set(LIBKLEO_VERSION "5.240.46") set(KLDAP_VERSION "5.240.40") set(LIBKDEPIM_VERSION "5.240.40") set(PIMTEXTEDIT_VERSION "5.240.40") set(PIMIDENTITYMANAGEMENT_VERSION "5.240.40") find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Core HttpServer Widgets PrintSupport) set_package_properties(Qt6 PROPERTIES TYPE REQUIRED PURPOSE "Basic application components" ) find_package(KF6 ${KF_MIN_VERSION} COMPONENTS Contacts Completion CoreAddons WidgetsAddons Config ColorScheme Codecs KIO XmlGui GuiAddons Kio Sonnet CalendarCore Archive) set_package_properties(KF6 PROPERTIES TYPE REQUIRED PURPOSE "Basic application components" ) find_package(KPim6Libkleo ${LIBKLEO_VERSION} CONFIG REQUIRED) find_package(KPim6LdapWidgets ${KLDAP_VERSION} CONFIG REQUIRED) find_package(KPim6Libkdepim ${LIBKDEPIM_LIB_VERSION} CONFIG REQUIRED) find_package(KPim6MimeTreeParserWidgets ${MIMETREEPARSER_VERSION} CONFIG REQUIRED) find_package(KPim6TextEdit ${PIMTEXTEDIT_VERSION} CONFIG REQUIRED) find_package(KPim6IdentityManagementWidgets ${PIMIDENTITYMANAGEMENT_VERSION} CONFIG REQUIRED) find_package(KPim6IdentityManagementCore ${PIMIDENTITYMANAGEMENT_VERSION} CONFIG REQUIRED) find_package(KF6TextAutoCorrectionCore CONFIG REQUIRED) +if (BUILD_TESTING) + find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test) +endif() + add_subdirectory(broker) add_subdirectory(server) diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 5e3ddc2..d7152c6 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -1,272 +1,278 @@ # SPDX-FileCopyrightText: 2023 g10 code GmbH # SPDX-Contributor: Carl Schwan # SPDX-License-Identifier: BSD-2-Clause add_library(gpgol-server-static) target_sources(gpgol-server-static PUBLIC websocketclient.cpp websocketclient.h + webserver.h + webserver.cpp # Identity identity/addressvalidationjob.cpp identity/addressvalidationjob.h identity/identitycombo.cpp identity/identitycombo.h identity/identitymanager.cpp identity/identitymanager.h identity/identitydialog.cpp identity/identitydialog.h identity/identityaddvcarddialog.cpp identity/identityaddvcarddialog.h # HTTP Controller controllers/emailcontroller.cpp controllers/emailcontroller.h # Draft draft/draft.cpp draft/draft.h draft/draftmanager.cpp draft/draftmanager.h # EWS integration ews/ewsattachment.cpp ews/ewsattachment.h ews/ewsattendee.cpp ews/ewsattendee.h ews/ewsclient_debug.cpp ews/ewsclient_debug.h ews/ewsid.cpp ews/ewsid.h ews/ewsitem.cpp ews/ewsitem.h ews/ewsitembase.cpp ews/ewsitembase.h ews/ewsitembase_p.h ews/ewsmailbox.cpp ews/ewsmailbox.h ews/ewsmailfactory.cpp ews/ewsmailfactory.h ews/ewsoccurrence.cpp ews/ewsoccurrence.h ews/ewspropertyfield.cpp ews/ewspropertyfield.h ews/ewsrecurrence.cpp ews/ewsrecurrence.h ews/ewsserverversion.cpp ews/ewsserverversion.h ews/ewstypes.cpp ews/ewstypes.h ews/ewsxml.cpp ews/ewsxml.h # Editor editor/composer.cpp editor/composer.h editor/composerviewbase.cpp editor/composerviewbase.h editor/composerwindow.cpp editor/composerwindow.h editor/composerwindowfactory.cpp editor/composerwindowfactory.h editor/cryptostateindicatorwidget.cpp editor/cryptostateindicatorwidget.h editor/encryptionstate.cpp editor/encryptionstate.h editor/kmcomposerglobalaction.cpp editor/kmcomposerglobalaction.h editor/nearexpirywarning.cpp editor/nearexpirywarning.h editor/mailtemplates.cpp editor/mailtemplates.h editor/recipient.cpp editor/recipient.h editor/recipientline.cpp editor/recipientline.h editor/recipientseditor.cpp editor/recipientseditor.h editor/util.h editor/util.cpp editor/richtextcomposerng.cpp editor/richtextcomposerng.h editor/richtextcomposersignatures.cpp editor/richtextcomposersignatures.h editor/nodehelper.cpp editor/nodehelper.h editor/signaturecontroller.cpp editor/signaturecontroller.h editor/spellcheckerconfigdialog.cpp editor/spellcheckerconfigdialog.h # Editor job editor/job/abstractencryptjob.h editor/job/autocryptheadersjob.h editor/job/contentjobbase.h editor/job/contentjobbase_p.h editor/job/dndfromarkjob.cpp editor/job/dndfromarkjob.h editor/job/encryptjob.h editor/job/inserttextfilejob.h editor/job/itipjob.h editor/job/jobbase.h editor/job/jobbase_p.h editor/job/maintextjob.h editor/job/multipartjob.h editor/job/protectedheadersjob.h editor/job/signencryptjob.h editor/job/signjob.h editor/job/singlepartjob.h editor/job/skeletonmessagejob.h editor/job/transparentjob.h editor/job/autocryptheadersjob.cpp editor/job/contentjobbase.cpp editor/job/encryptjob.cpp editor/job/inserttextfilejob.cpp editor/job/itipjob.cpp editor/job/jobbase.cpp editor/job/maintextjob.cpp editor/job/multipartjob.cpp editor/job/protectedheadersjob.cpp editor/job/saveasfilejob.cpp editor/job/saveasfilejob.h editor/job/signencryptjob.cpp editor/job/signjob.cpp editor/job/singlepartjob.cpp editor/job/skeletonmessagejob.cpp editor/job/transparentjob.cpp ## Editor Part editor/part/globalpart.h editor/part/infopart.h editor/part/itippart.h editor/part/messagepart.h editor/part/textpart.h editor/part/globalpart.cpp editor/part/infopart.cpp editor/part/itippart.cpp editor/part/messagepart.cpp editor/part/textpart.cpp ## Attachment editor/attachment/attachmentjob.cpp editor/attachment/attachmentjob.h editor/attachment/attachmentclipboardjob.cpp editor/attachment/attachmentclipboardjob.h editor/attachment/attachmentcompressjob.cpp editor/attachment/attachmentcompressjob.h editor/attachment/attachmentcontroller.cpp editor/attachment/attachmentcontroller.h editor/attachment/attachmentcontrollerbase.cpp editor/attachment/attachmentcontrollerbase.h editor/attachment/attachmentfromfolderjob.cpp editor/attachment/attachmentfromfolderjob.h editor/attachment/attachmentfrommimecontentjob.cpp editor/attachment/attachmentfrommimecontentjob.h editor/attachment/attachmentfromurlbasejob.cpp editor/attachment/attachmentfromurlbasejob.h editor/attachment/attachmentfromurljob.cpp editor/attachment/attachmentfromurljob.h editor/attachment/attachmentfromurlutils.cpp editor/attachment/attachmentfromurlutils.h editor/attachment/attachmentfrompublickeyjob.cpp editor/attachment/attachmentfrompublickeyjob.h editor/attachment/attachmentloadjob.cpp editor/attachment/attachmentloadjob.h editor/attachment/attachmentmodel.cpp editor/attachment/attachmentmodel.h editor/attachment/attachmentpart.cpp editor/attachment/attachmentpart.h editor/attachment/attachmentpropertiesdialog.cpp editor/attachment/attachmentpropertiesdialog.h editor/attachment/attachmentupdatejob.cpp editor/attachment/attachmentupdatejob.h editor/attachment/attachmentview.cpp editor/attachment/attachmentview.h editor/attachment/editorwatcher.cpp editor/attachment/editorwatcher.h ) ki18n_wrap_ui(gpgol-server-static editor/attachment/ui/attachmentpropertiesdialog.ui editor/attachment/ui/attachmentpropertiesdialog_readonly.ui ) ecm_qt_declare_logging_category(gpgol-server-static_SRCS HEADER websocket_debug.h IDENTIFIER WEBSOCKET_LOG CATEGORY_NAME org.gpgol.server.websocket DESCRIPTION "Websocket connection in the server" EXPORT GPGOL ) ecm_qt_declare_logging_category(gpgol-server-static_SRCS HEADER ewsresource_debug.h IDENTIFIER EWSRES_LOG CATEGORY_NAME org.gpgol.ews DESCRIPTION "Ews mail client" EXPORT GPGOL ) ecm_qt_declare_logging_category(gpgol-server-static_SRCS HEADER ewscli_debug.h IDENTIFIER EWSCLI_LOG CATEGORY_NAME org.gpgol.ews.client DESCRIPTION "ews client (gpgol-server)" EXPORT GPGOL ) ecm_qt_declare_logging_category(gpgol-server-static_SRCS HEADER editor_debug.h IDENTIFIER EDITOR_LOG CATEGORY_NAME org.gpgol.editor DESCRIPTION "mail composer" EXPORT GPGOL ) set(WARN_TOOMANY_RECIPIENTS_DEFAULT true) set(ALLOW_SEMICOLON_AS_ADDRESS_SEPARATOR_DEFAULT true) configure_file(editor/settings/messagecomposer.kcfg.in ${CMAKE_CURRENT_BINARY_DIR}/messagecomposer.kcfg) kconfig_add_kcfg_files(gpgol-server-static_SRCS editor/settings/messagecomposersettings.kcfgc ) install(FILES composerui.rc DESTINATION ${KDE_INSTALL_KXMLGUIDIR}/gpgol-server) target_sources(gpgol-server-static PUBLIC ${gpgol-server-static_SRCS}) target_link_libraries(gpgol-server-static PUBLIC Qt6::HttpServer Qt6::Widgets Qt6::PrintSupport KF6::CalendarCore KF6::ConfigCore KF6::ConfigGui KF6::Contacts KF6::Completion KF6::CoreAddons KF6::ColorScheme KF6::Codecs KF6::GuiAddons KF6::SonnetUi KF6::WidgetsAddons KF6::XmlGui KF6::KIOFileWidgets KF6::Archive KF6::TextAutoCorrectionCore KPim6::MimeTreeParserWidgets KPim6::Libkleo KPim6::Libkdepim KPim6::LdapWidgets KPim6::PimTextEdit KPim6::IdentityManagementCore KPim6::IdentityManagementWidgets ) add_executable(gpgol-server main.cpp) target_link_libraries(gpgol-server PRIVATE gpgol-server-static) + +if (BUILD_TESTING) + add_subdirectory(autotests) +endif() diff --git a/server/autotests/CMakeLists.txt b/server/autotests/CMakeLists.txt new file mode 100644 index 0000000..4842c3e --- /dev/null +++ b/server/autotests/CMakeLists.txt @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: 2023 g10 code GmbH +# SPDX-Contributor: Carl Schwan +# SPDX-License-Identifier: BSD-2-Clause + +add_definitions(-DDATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/testdata" ) + +ecm_add_test(emailcontrollertest.cpp + LINK_LIBRARIES gpgol-server-static Qt::Test + NAME_PREFIX "server-http-" +) diff --git a/server/autotests/emailcontrollertest.cpp b/server/autotests/emailcontrollertest.cpp new file mode 100644 index 0000000..243bebe --- /dev/null +++ b/server/autotests/emailcontrollertest.cpp @@ -0,0 +1,155 @@ +// SPDX-FileCopyrightText: 2023 g10 code GmbH +// SPDX-Contributor: Carl Schwan +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../webserver.h" +#include "../websocketclient.h" +#include "../draft/draftmanager.h" +#include "../editor/composerwindow.h" +#include "../editor/recipientseditor.h" + +#include +#include +#include + +using namespace Qt::Literals::StringLiterals; + +class EmailControllerTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase() + { + DraftManager::self(true); + QCoreApplication::setApplicationName(u"gpgol-server"_s); + + KLocalizedString::setApplicationDomain(QByteArrayLiteral("gpgol")); + + m_webServer = new WebServer; + m_webServer->run(); + + auto webSocketServer = new QWebSocketServer(QStringLiteral("SSL Server"), QWebSocketServer::NonSecureMode); + + + if (webSocketServer->listen(QHostAddress::Any, 5656)) { + + } + } + + void testInfoEmailAction() + { + QFile file(QStringLiteral(DATA_DIR) + u"/encrypted.mbox"_s); + QVERIFY(file.open(QIODeviceBase::ReadOnly)); + + QNetworkRequest request(QUrl(u"http://127.0.0.1:%1/info"_s.arg(m_webServer->port()))); + auto reply = m_qnam.post(request, file.readAll()); + QSignalSpy spy(reply, &QNetworkReply::finished); + spy.wait(); + + QVERIFY(reply->error() == QNetworkReply::NoError); + + const auto doc = QJsonDocument::fromJson(reply->readAll()); + QVERIFY(!doc.isNull() && doc.isObject()); + + const auto object = doc.object(); + QVERIFY(object["drafts"_L1].toArray().isEmpty()); + QVERIFY(object["encrypted"_L1].toBool()); + QVERIFY(!object["signed"_L1].toBool()); + } + + void testViewEmailAction() + { + QFile file(QStringLiteral(DATA_DIR) + u"/plaintext.mbox"_s); + QVERIFY(file.open(QIODeviceBase::ReadOnly)); + + QNetworkRequest request(QUrl(u"http://127.0.0.1:%1/view"_s.arg(m_webServer->port()))); + auto reply = m_qnam.post(request, file.readAll()); + QSignalSpy spy(reply, &QNetworkReply::finished); + spy.wait(); + + QVERIFY(reply->error() == QNetworkReply::NoError); + + const auto widgets = qApp->topLevelWidgets(); + QVERIFY(!widgets.isEmpty()); + + MimeTreeParser::Widgets::MessageViewerDialog *dialog = nullptr; + for (auto widget : widgets) { + if (!widget->isHidden()) { + if (const auto messageViewer = qobject_cast(widget)) { + dialog = messageViewer; + break; + } + } + } + + QVERIFY(dialog); + + WebsocketClient::self(QUrl(u"http://127.0.0.1"_s), 5656); + + const auto toolBar = dialog->toolBar(); + QVERIFY(toolBar->isVisible()); + + const auto actions = toolBar->actions(); + QCOMPARE(actions.count(), 3); + qWarning() << actions; + + QCOMPARE(actions[1]->icon().name(), u"mail-reply-sender-symbolic"_s); + actions[1]->trigger(); + + const auto widgets2 = qApp->topLevelWidgets(); + QVERIFY(!widgets2.isEmpty()); + + ComposerWindow *composer = nullptr; + for (auto widget : widgets2) { + if (!widget->isHidden()) { + if (const auto composerWindow = qobject_cast(widget)) { + composer = composerWindow; + break; + } + } + } + QVERIFY(composer); + + QSignalSpy spyInit(composer, &ComposerWindow::initialized); + spyInit.wait(); + + QCOMPARE(composer->subject(), u"RE: A random subject with alternative contenttype"_s); + + const auto recipients = composer->recipientsEditor()->recipients(); + QCOMPARE(recipients.count(), 2); + QCOMPARE(recipients[0]->email(), u"konqi@example.org"_s); + QCOMPARE(recipients[0]->name(), u"Konqi"_s); + + QCOMPARE(recipients[1]->email(), u"konqi@kde.org"_s); + QVERIFY(recipients[1]->name().isEmpty()); + } + + void cleanupTestCase() + { + m_webServer->deleteLater(); + } + +private: + QThread *m_thread = nullptr; + WebServer *m_webServer = nullptr; + QWebSocketServer *m_webSocketServer = nullptr; + QNetworkAccessManager m_qnam; +}; + +QTEST_MAIN(EmailControllerTest) +#include "emailcontrollertest.moc" diff --git a/server/autotests/testdata/encrypted.mbox b/server/autotests/testdata/encrypted.mbox new file mode 100644 index 0000000..b08783d --- /dev/null +++ b/server/autotests/testdata/encrypted.mbox @@ -0,0 +1,59 @@ +From carl@carlschwan.eu Mon Nov 13 08:25:04 2023 +From: Carl Schwan +To: carl.schwan@gnupg.com +Subject: Secret +Date: Mon, 13 Nov 2023 09:25:04 +0100 +Message-ID: <2887513.mvXUDI8C0e@fedora> +X-KMail-Identity: 38354086 +X-KMail-Transport: 1303906102 +X-KMail-Fcc: 154 +X-KMail-Identity-Name: Fastmail +X-KMail-Transport-Name: Fastmail +MIME-Version: 1.0 +Content-Type: multipart/encrypted; boundary="nextPart5908474.DvuYhMxLoT"; + protocol="application/pgp-encrypted" + +--nextPart5908474.DvuYhMxLoT +Content-Type: application/pgp-encrypted +Content-Disposition: attachment +Content-Transfer-Encoding: 7Bit + +Version: 1 +--nextPart5908474.DvuYhMxLoT +Content-Type: application/octet-stream +Content-Disposition: inline; filename="msg.asc" +Content-Transfer-Encoding: 7Bit + +-----BEGIN PGP MESSAGE----- + +hF4DETvd3+xd3hYSAQdAnw+hknvbfthJiWtxFt6XoItAk2VM3Z47pSMYr45RFG8w +0ljCxV4Z+NVjzF93AJDG/YTgphgI3Q/9qxi38M7kZIP2NqyPIa4tYSbE3ki7GX+Z +hQEMAxicO9e1rm0dAQf/XEAydeuDlk6fDVBlGfNCrp7XcSFv95jFVH9+qnlQkJQC +2QTGtMqS18MRYswQFidOdiowLqI/tu8YXHhBrFLV7fF3i5opiiyWEr0kMIwn1qU1 +sDakOriVRyz/Znrow5lkaB2CYF5j++6jT9YH1QmrGxuly67wP4zj4pE3v7M42CtF +BJHPEwbnISuurle4agBpw210eqzVcwmvJ4MdnbrDHAJ7nkL6OPGAlExHFU0xUrrN +geq8Np+FwoVMrYwHNSRrJAxCJL69h034J4/juUVHcX07THpZWyJaPTnQXJ7nW++Q +swC8fqPlIFnqNnvtDYXTAMGB2fPfGVpi69dlZPA88NTpAQkCEExsj54vmVWyFi/i +gEAcBZM5WlPTnzgYy/ieckQZ/luZgdURzPD0AcH8InuD79+TXpLawIDI9tx1601l +8LoL2c00rJ1RV6NDdQ2i4PHLZFKzkvNmrhs5Bzc0Lrinz3evHXksJsMkdMdQWTlP ++u+WNXUTseyTAm1NSF7nnn1+7bhDr4Y8MzSpagtpH0gRjDdLefKXbhvbjkTay3rU +73gUg5NphHb+qvCrBAX5XoWIywwdQI+7HyzU/yDg2DpnnNXvohtARz0tr+1GIhSh +1ojVD6UWRCPxraMjA4kXtPwGlYLN/2ftlDq1qK87/s4qXWcQBOi9oReW9KyUQdWG +FDz7ND9HCQhQjF8yHLC+jBc/DdlpRFQeEhaVVqcV7SnbshVPo/hp6rvpo6QC+Lj2 +xWIyNjaZFLRKCoQL8RlltysMdWRxvrPAaJYX3BM0RFPU3KoaafEwJP2r64ElBzgV +Mqaye57pVfApJ5s8+us3+PB1mAK0i3Fmrv9UeZh+IsDDo5Qki1NPaR0mA+zL4eb6 +cOLVORgBNcgAb+mci1Eb7IXQUnPJXaT/SDsOpVM/M7u+RVOniWwXCTY6JrBVIvNb +IFDCvWKQYjlTFEk/zrMCal2UzoSESqUTqJZ2r9IJevhkZFBfYQKq3XBe+unHgHVI ++xQUZQtiQZDQe4h6qn8KHGjAl9XkB8jNrl6zw+oXwFU3bUnZvmiGCLBlK4tLT21N +qO8ANike35dLb2D1Xi3uJgOGo2+wETVj+IcEXbXdz2CjE2AgEsWs/Q4qVyPMPIFO +WAcs7wQZRmS7ORMkzXqQUOkqHGRbbyr4cvbLVk7oTjDGXQegychuLdqePUpymEVz +a1640SXfEi7T6KcmJgjklXIRLp4astUSwmwCbSG6m3iFriuiasoJezjugrjGkcyP +BHmZnBMDMgvB17n4mi7MfobrlnLKB/Uz0e0zngfoPLxsZSFhaThYsQNEKoaRfQUe +BfEOs6T0IBVdNyshfl9AtIB0yofLOaKmitX7HrE8ntaz0pysAcrq3YxTkfHOW9Q5 +v1krcpAdyLxWE112pIt+862Besy+J08dzoTTID/+wFtir2XByq7RKWuQPB0e/CHQ +TNH0ly3JOlKsLTH66PTTfHQv721n/5+s0iY= +=WbE3 +-----END PGP MESSAGE----- + +--nextPart5908474.DvuYhMxLoT-- + diff --git a/server/autotests/testdata/plaintext.mbox b/server/autotests/testdata/plaintext.mbox new file mode 100644 index 0000000..d185b1c --- /dev/null +++ b/server/autotests/testdata/plaintext.mbox @@ -0,0 +1,13 @@ +Return-Path: +Date: Wed, 8 Jun 2016 20:34:44 -0700 +From: Konqi +To: konqi@kde.org +Subject: A random subject with alternative contenttype +MIME-Version: 1.0 +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: quoted-printable + +If you can see this text it means that your email client couldn't display o= +ur newsletter properly. +Please visit this link to view the newsletter on our website: http://www.go= +g.com/newsletter/ diff --git a/server/draft/draftmanager.cpp b/server/draft/draftmanager.cpp index 4a698e4..02395c4 100644 --- a/server/draft/draftmanager.cpp +++ b/server/draft/draftmanager.cpp @@ -1,72 +1,78 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "draftmanager.h" #include #include #include "editor_debug.h" -DraftManager::DraftManager() +DraftManager::DraftManager(bool testMode) + : m_testMode(testMode) { - const QDir directory(draftDirectory()); + const QDir directory(draftDirectory(testMode)); const auto entries = directory.entryList(QDir::Files); for (const QString &entry : entries) { Draft draft(draftDirectory() + entry); if (draft.isValid()) { m_drafts << draft; } else { qFatal(EDITOR_LOG) << "File does not exist or is not readable" << entry ; } } } -QString DraftManager::draftDirectory() +QString DraftManager::draftDirectory(bool testMode) { - static const QString path = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/gpgol-server/autosave/"); - return path; + if (testMode) { + static const QString path = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + QLatin1String("/gpgol-server/autosave/"); + return path; + } else { + static const QString path = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/gpgol-server/autosave/"); + return path; + } } -DraftManager &DraftManager::self() +DraftManager &DraftManager::self(bool testMode) { - static DraftManager s_draftManager; + static DraftManager s_draftManager(testMode); return s_draftManager; } QList DraftManager::drafts() const { return m_drafts; } QJsonArray DraftManager::toJson() const { if (m_drafts.isEmpty()) { return {}; } QJsonArray array; std::transform(m_drafts.cbegin(), m_drafts.cend(), std::back_inserter(array), [](const auto draft) { return draft.toJson(); }); return array; } bool DraftManager::remove(const Draft &draft) { auto it = std::find(m_drafts.begin(), m_drafts.end(), draft); if (it == m_drafts.end()) { return false; } bool ok = it->remove(); m_drafts.erase(it); return ok; } Draft DraftManager::draftById(const QByteArray &draftId) { return Draft(draftDirectory() + QString::fromUtf8(draftId)); } diff --git a/server/draft/draftmanager.h b/server/draft/draftmanager.h index 0b40232..b55bcef 100644 --- a/server/draft/draftmanager.h +++ b/server/draft/draftmanager.h @@ -1,35 +1,39 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "draft.h" #include /// @class DraftManager /// /// Manage the email drafts of the user. class DraftManager { public: /// Get the DraftManager singleton. - static DraftManager &self(); + /// \param testMode Set it to true when running unit tests. + static DraftManager &self(bool testMode = false); - static QString draftDirectory(); + /// Get the directory where the drafts are stored. + /// \param testMode Set it to true when running unit tests. + static QString draftDirectory(bool testMode = false); /// List of drafts. [[nodiscard]] QList drafts() const; /// List of drafts as JSON array. [[nodiscard]] QJsonArray toJson() const; /// Remove the specified dradt from the filesystem. bool remove(const Draft &draft); /// Get a draft by it's id. Draft draftById(const QByteArray &draftId); private: - DraftManager(); + DraftManager(bool testMode); QList m_drafts; + bool m_testMode = false; }; \ No newline at end of file diff --git a/server/editor/composerwindow.cpp b/server/editor/composerwindow.cpp index 60d1031..f39bbdb 100644 --- a/server/editor/composerwindow.cpp +++ b/server/editor/composerwindow.cpp @@ -1,1570 +1,1590 @@ // 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 #include #include #include // Gpgme includes #include #include // App includes #include "../identity/identitymanager.h" #include "../identity/identitycombo.h" #include "../identity/identitydialog.h" #include "recipientseditor.h" #include "nearexpirywarning.h" #include "cryptostateindicatorwidget.h" #include "composerviewbase.h" #include "richtextcomposerng.h" #include "signaturecontroller.h" #include "job/dndfromarkjob.h" #include "job/saveasfilejob.h" #include "job/inserttextfilejob.h" #include "attachment/attachmentcontroller.h" #include "attachment/attachmentview.h" #include "attachment/attachmentmodel.h" #include "kmcomposerglobalaction.h" #include "mailtemplates.h" #include "messagecomposersettings.h" #include "spellcheckerconfigdialog.h" #include "websocketclient.h" #include "draft/draftmanager.h" using namespace Qt::Literals::StringLiterals; namespace { auto findSendersUid(const std::string &addrSpec, const std::vector &userIds) { return std::find_if(userIds.cbegin(), userIds.cend(), [&addrSpec](const auto &uid) { return uid.addrSpec() == addrSpec || (uid.addrSpec().empty() && std::string(uid.email()) == addrSpec) || (uid.addrSpec().empty() && (!uid.email() || !*uid.email()) && uid.name() == addrSpec); }); } } 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)) , mCryptoStateIndicatorWidget(new CryptoStateIndicatorWidget(mHeadersArea)) , mRichTextComposer(new MessageComposer::RichTextComposerNg(this)) , mRichTextEditorWidget(new TextCustomEditor::RichTextEditorWidget(mRichTextComposer, mMainWidget)) , mNearExpiryWarning(new NearExpiryWarning(this)) , mGlobalAction(new KMComposerGlobalAction(this, this)) , mKeyCache(Kleo::KeyCache::mutableInstance()) { bool isNew = false; mIdentity = IdentityManager::self().fromEmail(from, isNew); mEdtFrom = new IdentityCombo(mHeadersArea); if (isNew) { // fill the idenity with default fields auto dlg = new KMail::IdentityDialog; mIdentity.setFullName(name); dlg->setIdentity(mIdentity); connect(dlg, &KMail::IdentityDialog::keyListingFinished, this, [this, dlg]() { dlg->updateIdentity(mIdentity); IdentityManager::self().updateIdentity(mIdentity); }); } mComposerBase->setBearerToken(bearerToken); mMainWidget->resize(800, 600); setCentralWidget(mMainWidget); setWindowTitle(i18nc("@title:window", "Composer")); setMinimumSize(200, 200); mHeadersToEditorSplitter->setObjectName(QStringLiteral("mHeadersToEditorSplitter")); mHeadersToEditorSplitter->setChildrenCollapsible(false); auto v = new QVBoxLayout(mMainWidget); v->setContentsMargins({}); v->addWidget(mNearExpiryWarning); v->addWidget(mHeadersToEditorSplitter); mHeadersArea->setSizePolicy(mHeadersToEditorSplitter->sizePolicy().horizontalPolicy(), QSizePolicy::Expanding); mHeadersToEditorSplitter->addWidget(mHeadersArea); const QList defaultSizes{0}; mHeadersToEditorSplitter->setSizes(defaultSizes); mGrid->setColumnStretch(0, 1); mGrid->setColumnStretch(1, 100); mGrid->setRowStretch(3 + 1, 100); int row = 0; // From mLblFrom->setObjectName(QStringLiteral("fromLineLabel")); mLblFrom->setFixedWidth(mRecipientEditor->setFirstColumnWidth(0)); mLblFrom->setBuddy(mEdtFrom); auto fromWrapper = new QWidget(mHeadersArea); auto fromWrapperLayout = new QHBoxLayout(fromWrapper); fromWrapperLayout->setContentsMargins({}); mEdtFrom->installEventFilter(this); mEdtFrom->setCurrentIdentity(mFrom); mEdtFrom->setObjectName(QStringLiteral("fromLine")); connect(mEdtFrom, &QComboBox::currentIndexChanged, this, &ComposerWindow::slotIdentityChanged); fromWrapperLayout->addWidget(mEdtFrom); mComposerBase->setIdentityCombo(mEdtFrom); mComposerBase->setIdentityManager(&IdentityManager::self()); mButtonFrom->setText(i18nc("@action:button", "Configure")); mButtonFrom->setIcon(QIcon::fromTheme(u"configure-symbolic"_s)); connect(mButtonFrom, &QPushButton::clicked, this, &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); row++; // Subject mEdtSubject->setObjectName(u"subjectLine"_s); mLblSubject->setObjectName(u"subjectLineLabel"_s); mLblSubject->setBuddy(mEdtSubject); mGrid->addWidget(mLblSubject, row, 0); mGrid->addWidget(mEdtSubject, row, 1); row++; auto editorWidget = new QWidget(); auto vLayout = new QVBoxLayout(editorWidget); vLayout->setContentsMargins({}); vLayout->setSpacing(0); mHeadersToEditorSplitter->addWidget(editorWidget); // Crypto indicator vLayout->addWidget(mCryptoStateIndicatorWidget); mCryptoStateIndicatorWidget->setObjectName(QStringLiteral("CryptoIndicator")); // Message widget auto connectionLossWidget = new KMessageWidget(this); connectionLossWidget->hide(); connectionLossWidget->setWordWrap(true); connectionLossWidget->setPosition(KMessageWidget::Position::Header); vLayout->addWidget(connectionLossWidget); auto &websocketClient = WebsocketClient::self(); connect(&websocketClient, &WebsocketClient::closed, this, [connectionLossWidget](const QString &errorMessage) { connectionLossWidget->setText(errorMessage); connectionLossWidget->show(); }); connect(&websocketClient, &WebsocketClient::connected, this, [connectionLossWidget]() { connectionLossWidget->hide(); }); connect(&websocketClient, &WebsocketClient::emailSentSuccessfully, this, [this](const QString &id) { if (id == mComposerBase->mailId()) { auto &draftManager = DraftManager::self(); draftManager.remove(draftManager.draftById(id.toUtf8())); 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); updateSignatureAndEncryptionStateIndicators(); toolBar(u"mainToolBar"_s)->show(); connect(expiryChecker().get(), &Kleo::ExpiryChecker::expiryMessage, this, [&](const GpgME::Key &key, QString msg, Kleo::ExpiryChecker::ExpiryInformation info, bool isNewMessage) { Q_UNUSED(isNewMessage); if (info == Kleo::ExpiryChecker::OwnKeyExpired || info == Kleo::ExpiryChecker::OwnKeyNearExpiry) { const auto plainMsg = msg.replace(QStringLiteral("

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

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

"), QStringLiteral(" ")); mNearExpiryWarning->addInfo(plainMsg); mNearExpiryWarning->setWarning(info == Kleo::ExpiryChecker::OwnKeyExpired); mNearExpiryWarning->animatedShow(); } const QList lstLines = mRecipientEditor->lines(); for (KPIM::MultiplyingLine *line : lstLines) { auto recipient = line->data().dynamicCast(); if (recipient->key().primaryFingerprint() == key.primaryFingerprint()) { auto recipientLine = qobject_cast(line); QString iconname = QStringLiteral("emblem-warning"); if (info == Kleo::ExpiryChecker::OtherKeyExpired) { mEncryptionState.setAcceptedSolution(false); iconname = QStringLiteral("emblem-error"); const auto showCryptoIndicator = true; const auto hasOverride = mEncryptionState.hasOverride(); const auto encrypt = mEncryptionState.encrypt(); const bool showAllIcons = showCryptoIndicator && hasOverride && encrypt; if (!showAllIcons) { recipientLine->setIcon(QIcon(), msg); return; } } recipientLine->setIcon(QIcon::fromTheme(iconname), msg); return; } } }); // TODO make it possible to show this auto dictionaryComboBox = new Sonnet::DictionaryComboBox(this); dictionaryComboBox->hide(); mComposerBase->setDictionary(dictionaryComboBox); slotIdentityChanged(); } void 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(&mEncryptionState, &EncryptionState::possibleEncryptChanged, mEncryptAction, &KToggleAction::setEnabled); connect(mEncryptAction, &KToggleAction::triggered, &mEncryptionState, &EncryptionState::toggleOverride); // Signing action mSignAction = new KToggleAction(QIcon::fromTheme(QStringLiteral("document-sign")), i18n("&Sign Message"), this); mSignAction->setIconText(i18n("Sign")); actionCollection()->addAction(QStringLiteral("sign_message"), mSignAction); connect(&mEncryptionState, &EncryptionState::possibleEncryptChanged, mEncryptAction, &KToggleAction::setEnabled); connect(&mEncryptionState, &EncryptionState::encryptChanged, mEncryptAction, &KToggleAction::setChecked); connect(mSignAction, &KToggleAction::triggered, this, &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); connect(&mEncryptionState, &EncryptionState::encryptChanged, this, &ComposerWindow::slotEncryptionButtonIconUpdate); connect(&mEncryptionState, &EncryptionState::encryptChanged, this, &ComposerWindow::updateSignatureAndEncryptionStateIndicators); connect(&mEncryptionState, &EncryptionState::overrideChanged, this, &ComposerWindow::slotEncryptionButtonIconUpdate); connect(&mEncryptionState, &EncryptionState::overrideChanged, this, &ComposerWindow::runKeyResolver); connect(&mEncryptionState, &EncryptionState::acceptedSolutionChanged, this, &ComposerWindow::slotEncryptionButtonIconUpdate); } 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, true); } 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); if (!setByUser) { updateSignatureAndEncryptionStateIndicators(); } // 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); return keyResolverCore; } void ComposerWindow::slotEncryptionButtonIconUpdate() { const auto state = mEncryptionState.encrypt(); const auto setByUser = mEncryptionState.override(); const auto acceptedSolution = mEncryptionState.acceptedSolution(); auto icon = QIcon::fromTheme(QStringLiteral("document-encrypt")); QString tooltip; if (state) { tooltip = i18nc("@info:tooltip", "Encrypt"); } else { tooltip = i18nc("@info:tooltip", "Not Encrypt"); icon = QIcon::fromTheme(QStringLiteral("document-decrypt")); } if (acceptedSolution) { auto overlay = QIcon::fromTheme(QStringLiteral("emblem-added")); if (state) { overlay = QIcon::fromTheme(QStringLiteral("emblem-checked")); } icon = KIconUtils::addOverlay(icon, overlay, Qt::BottomRightCorner); } else { if (state && setByUser) { auto overlay = QIcon::fromTheme(QStringLiteral("emblem-warning")); icon = KIconUtils::addOverlay(icon, overlay, Qt::BottomRightCorner); } } mEncryptAction->setIcon(icon); mEncryptAction->setToolTip(tooltip); } void 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) { mEncryptionState.setAcceptedSolution(false); return; } } mEncryptionState.setAcceptedSolution(result.flags & Kleo::KeyResolverCore::AllResolved); for (auto line_ : lst) { auto line = qobject_cast(line_); Q_ASSERT(line); auto recipient = line->data().dynamicCast(); QString dummy; QString addrSpec; if (KEmailAddress::splitAddress(recipient->email(), dummy, addrSpec, dummy) != KEmailAddress::AddressOk) { addrSpec = recipient->email(); } auto resolvedKeys = result.solution.encryptionKeys[addrSpec]; GpgME::Key key; if (resolvedKeys.size() == 0) { // no key found for recipient // Search for any key, also for not accepted ons, to at least give the user more info. key = Kleo::KeyCache::instance()->findBestByMailBox(addrSpec.toUtf8().constData(), GpgME::UnknownProtocol, Kleo::KeyCache::KeyUsage::Encrypt); key.update(); // We need tofu information for key. recipient->setKey(key); } else { // A key was found for recipient key = resolvedKeys.front(); if (recipient->key().primaryFingerprint() != key.primaryFingerprint()) { key.update(); // We need tofu information for key. recipient->setKey(key); } } annotateRecipientEditorLineWithCrpytoInfo(line); if (!key.isNull()) { mExpiryChecker->checkKey(key, Kleo::ExpiryChecker::EncryptionKey); } } } void ComposerWindow::annotateRecipientEditorLineWithCrpytoInfo(RecipientLineNG *line) { auto recipient = line->data().dynamicCast(); const auto key = recipient->key(); const auto showCryptoIndicator = true; const auto hasOverride = mEncryptionState.hasOverride(); const auto encrypt = mEncryptionState.encrypt(); const bool showPositiveIcons = showCryptoIndicator && encrypt; const bool showAllIcons = showCryptoIndicator && hasOverride && encrypt; QString dummy; QString addrSpec; bool invalidEmail = false; if (KEmailAddress::splitAddress(recipient->email(), dummy, addrSpec, dummy) != KEmailAddress::AddressOk) { invalidEmail = true; addrSpec = recipient->email(); } if (key.isNull()) { recipient->setEncryptionAction(Kleo::Impossible); if (showAllIcons && !invalidEmail) { const auto icon = QIcon::fromTheme(QStringLiteral("emblem-error")); line->setIcon(icon, i18nc("@info:tooltip", "No key found for the recipient.")); } else { line->setIcon(QIcon()); } line->setProperty("keyStatus", invalidEmail ? InProgress : NoKey); return; } CryptoKeyState keyState = KeyOk; if (recipient->encryptionAction() != Kleo::DoIt) { recipient->setEncryptionAction(Kleo::DoIt); } QString tooltip; const auto uids = key.userIDs(); const auto _uid = findSendersUid(addrSpec.toStdString(), uids); GpgME::UserID uid; if (_uid == uids.cend()) { uid = key.userID(0); } else { uid = *_uid; } const auto trustLevel = Kleo::trustLevel(uid); switch (trustLevel) { case Kleo::Level0: if (uid.tofuInfo().isNull()) { tooltip = i18nc("@info:tooltip", "The encryption key is not trusted. It hasn't enough validity. " "You can sign the key, if you communicated the fingerprint by another channel. " "Click the icon for details."); keyState = NoKey; } else { switch (uid.tofuInfo().validity()) { case GpgME::TofuInfo::NoHistory: tooltip = i18nc("@info:tooltip", "The encryption key is not trusted. " "It hasn't been used anywhere to guarantee it belongs to the stated person. " "By using the key will be trusted more. " "Or you can sign the key, if you communicated the fingerprint by another channel. " "Click the icon for details."); break; case GpgME::TofuInfo::Conflict: tooltip = i18nc("@info:tooltip", "The encryption key is not trusted. It has conflicting TOFU data. " "Click the icon for details."); keyState = NoKey; break; case GpgME::TofuInfo::ValidityUnknown: tooltip = i18nc("@info:tooltip", "The encryption key is not trusted. It has unknown validity in TOFU data. " "Click the icon for details."); keyState = NoKey; break; default: tooltip = i18nc("@info:tooltip", "The encryption key is not trusted. The key is marked as bad. " "Click the icon for details."); keyState = NoKey; } } break; case Kleo::Level1: tooltip = i18nc("@info:tooltip", "The encryption key is only marginally trusted and hasn't been used enough time to guarantee it belongs to the stated person. " "By using the key will be trusted more. " "Or you can sign the key, if you communicated the fingerprint by another channel. " "Click the icon for details."); break; case Kleo::Level2: if (uid.tofuInfo().isNull()) { tooltip = i18nc("@info:tooltip", "The encryption key is only marginally trusted. " "You can sign the key, if you communicated the fingerprint by another channel. " "Click the icon for details."); } else { tooltip = i18nc("@info:tooltip", "The encryption key is only marginally trusted, but has been used enough times to be very likely controlled by the stated person. " "By using the key will be trusted more. " "Or you can sign the key, if you communicated the fingerprint by another channel. " "Click the icon for details."); } break; case Kleo::Level3: tooltip = i18nc("@info:tooltip", "The encryption key is fully trusted. You can raise the security level, by signing the key. " "Click the icon for details."); break; case Kleo::Level4: tooltip = i18nc("@info:tooltip", "The encryption key is ultimately trusted or is signed by another ultimately trusted key. " "Click the icon for details."); break; default: Q_UNREACHABLE(); } if (keyState == NoKey) { mEncryptionState.setAcceptedSolution(false); if (showAllIcons) { line->setIcon(QIcon::fromTheme(QStringLiteral("emblem-error")), tooltip); } else { line->setIcon(QIcon()); } } else if (trustLevel == Kleo::Level0 && encrypt) { line->setIcon(QIcon::fromTheme(QStringLiteral("emblem-warning")), tooltip); } else if (showPositiveIcons) { // Magically, the icon name maps precisely to each trust level // line->setIcon(QIcon::fromTheme(QStringLiteral("gpg-key-trust-level-%1").arg(trustLevel)), tooltip); line->setIcon(QIcon::fromTheme(QStringLiteral("emblem-success")), tooltip); } else { line->setIcon(QIcon()); } if (line->property("keyStatus") != keyState) { line->setProperty("keyStatus", keyState); } } void ComposerWindow::slotSignToggled(bool on) { setSigning(on, true); updateSignatureAndEncryptionStateIndicators(); } void ComposerWindow::updateSignatureAndEncryptionStateIndicators() { mCryptoStateIndicatorWidget->updateSignatureAndEncrypionStateIndicators(sign(), mEncryptionState.encrypt()); } 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 = mEncryptionState.encrypt(); mComposerBase->setCryptoOptions( sign(), encrypt, cryptoMessageFormat()); mComposerBase->send(); } void ComposerWindow::changeCryptoAction() { if (!QGpgME::openpgp() && !QGpgME::smime()) { // no crypto whatsoever mEncryptAction->setEnabled(false); mEncryptionState.setPossibleEncrypt(false); mSignAction->setEnabled(false); setSigning(false); } else { // TODO: carl const bool canOpenPGPSign = QGpgME::openpgp();// && !ident.pgpSigningKey().isEmpty(); const bool canSMIMESign = QGpgME::smime(); // && !ident.smimeSigningKey().isEmpty(); setSigning((canOpenPGPSign || canSMIMESign)); // && ident.pgpAutoSign()); } } void 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() { mLastIdentityHasSigningKey = !mIdentity.pgpSigningKey().isEmpty() || !mIdentity.smimeSigningKey().isEmpty(); mLastIdentityHasEncryptionKey = !mIdentity.pgpEncryptionKey().isEmpty() || !mIdentity.smimeEncryptionKey().isEmpty(); mComposerBase->signatureController()->setIdentity(mIdentity); mComposerBase->editor()->setAutocorrectionLanguage(mIdentity.autocorrectionLanguage()); mComposerBase->dictionary()->setCurrentByDictionaryName(mIdentity.dictionary()); mComposerBase->editor()->setSpellCheckingLanguage(mComposerBase->dictionary()->currentDictionary()); bool bPGPEncryptionKey = !mIdentity.pgpEncryptionKey().isEmpty(); bool bPGPSigningKey = !mIdentity.pgpSigningKey().isEmpty(); bool bSMIMEEncryptionKey = !mIdentity.smimeEncryptionKey().isEmpty(); bool bSMIMESigningKey = !mIdentity.smimeSigningKey().isEmpty(); if (cryptoMessageFormat() & Kleo::AnyOpenPGP) { if (bPGPEncryptionKey) { auto const key = mKeyCache->findByKeyIDOrFingerprint(mIdentity.pgpEncryptionKey().constData()); if (key.isNull() || !key.canEncrypt()) { bPGPEncryptionKey = false; } } if (bPGPSigningKey) { auto const key = mKeyCache->findByKeyIDOrFingerprint(mIdentity.pgpSigningKey().constData()); if (key.isNull() || !key.canSign()) { bPGPSigningKey = false; } } } else { bPGPEncryptionKey = false; bPGPSigningKey = false; } if (cryptoMessageFormat() & Kleo::AnySMIME) { if (bSMIMEEncryptionKey) { auto const key = mKeyCache->findByKeyIDOrFingerprint(mIdentity.smimeEncryptionKey().constData()); if (key.isNull() || !key.canEncrypt()) { bSMIMEEncryptionKey = false; } } if (bSMIMESigningKey) { auto const key = mKeyCache->findByKeyIDOrFingerprint(mIdentity.smimeSigningKey().constData()); if (key.isNull() || !key.canSign()) { bSMIMESigningKey = false; } } } else { bSMIMEEncryptionKey = false; bSMIMESigningKey = false; } bool bNewIdentityHasSigningKey = bPGPSigningKey || bSMIMESigningKey; bool bNewIdentityHasEncryptionKey = bPGPEncryptionKey || bSMIMEEncryptionKey; if (!mKeyCache->initialized()) { // We need to start key listing on our own othweise KMail will crash and we want to wait till the cache is populated. mKeyCache->startKeyListing(); connect(mKeyCache.get(), &Kleo::KeyCache::keyListingDone, this, [this]() { checkOwnKeyExpiry(mIdentity); runKeyResolver(); }); } else { checkOwnKeyExpiry(mIdentity); } // save the state of the sign and encrypt button if (!bNewIdentityHasEncryptionKey && mLastIdentityHasEncryptionKey) { mLastEncryptActionState = mEncryptionState.encrypt(); } mSignAction->setEnabled(bNewIdentityHasSigningKey); if (!bNewIdentityHasSigningKey && mLastIdentityHasSigningKey) { mLastSignActionState = sign(); setSigning(false); } // restore the last state of the sign and encrypt button if (bNewIdentityHasSigningKey && !mLastIdentityHasSigningKey) { setSigning(mLastSignActionState); } mLastIdentityHasSigningKey = bNewIdentityHasSigningKey; mLastIdentityHasEncryptionKey = bNewIdentityHasEncryptionKey; const KIdentityManagementCore::Signature sig = const_cast(mIdentity).signature(); bool isEnabledSignature = sig.isEnabledSignature(); mAppendSignature->setEnabled(isEnabledSignature); mPrependSignature->setEnabled(isEnabledSignature); mInsertSignatureAtCursorPosition->setEnabled(isEnabledSignature); mEncryptionState.setPossibleEncrypt(true); changeCryptoAction(); mEncryptionState.unsetOverride(); mEncryptionState.setPossibleEncrypt(mEncryptionState.possibleEncrypt() && bNewIdentityHasEncryptionKey); mEncryptionState.setAutoEncrypt(mIdentity.pgpAutoEncrypt()); // make sure the From and BCC fields are shown if necessary if (mIdentity.pgpAutoEncrypt() && mKeyCache->initialized()) { runKeyResolver(); } Q_EMIT identityChanged(); } void 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(); } diff --git a/server/editor/composerwindow.h b/server/editor/composerwindow.h index 1575895..04b6ed5 100644 --- a/server/editor/composerwindow.h +++ b/server/editor/composerwindow.h @@ -1,213 +1,224 @@ // 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 // App includes #include "encryptionstate.h" #include "composerwindowfactory.h" class QSplitter; class QLabel; class QPrinter; class QGridLayout; class QLineEdit; class QPushButton; class KLineEdit; class RecipientsEditor; class KToggleAction; class CryptoStateIndicatorWidget; class RecipientLineNG; class NearExpiryWarning; class IdentityCombo; class KMComposerGlobalAction; class KRecentFilesAction; namespace KPIMTextEdit { class RichTextComposerWidget; class RichTextComposer; } namespace TextCustomEditor { class RichTextEditorWidget; } namespace MessageComposer { class ComposerViewBase; class RichTextComposerNg; } namespace Kleo { class KeyResolverCore; class ExpiryChecker; } class 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 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 annotateRecipientEditorLineWithCrpytoInfo(RecipientLineNG *line); void setupActions(); void setupStatusBar(QWidget *w); void htmlToolBarVisibilityChanged(bool visible); void changeCryptoAction(); void runKeyResolver(); int validateLineWrapWidth() const; void disableWordWrap(); void checkOwnKeyExpiry(const KIdentityManagementCore::Identity &ident); std::unique_ptr fillKeyResolver(); /** * Returns true if the message was modified by the user. */ [[nodiscard]] bool isModified() const; /** * Returns true if the message will be signed. */ [[nodiscard]] bool sign() const; Kleo::CryptoMessageFormat cryptoMessageFormat() const; void updateSignatureAndEncryptionStateIndicators(); KIdentityManagementCore::Identity mIdentity; QString mFrom; QWidget *const mMainWidget; // splitter between the headers area and the actual editor MessageComposer::ComposerViewBase *const mComposerBase; QSplitter *const mHeadersToEditorSplitter; QWidget *const mHeadersArea; QGridLayout *const mGrid; QLabel *const mLblFrom; QPushButton *const mButtonFrom; RecipientsEditor *const mRecipientEditor; QLabel *const mLblSubject; QLineEdit *const mEdtSubject; CryptoStateIndicatorWidget *const mCryptoStateIndicatorWidget; MessageComposer::RichTextComposerNg *const mRichTextComposer; TextCustomEditor::RichTextEditorWidget *const mRichTextEditorWidget; NearExpiryWarning *const mNearExpiryWarning; KMComposerGlobalAction *const mGlobalAction; IdentityCombo * mEdtFrom = nullptr; bool mForceDisableHtml = false; bool mLastIdentityHasSigningKey = false; bool mLastIdentityHasEncryptionKey = false; QAction *mEncryptAction = nullptr; QAction *mSignAction = nullptr; QAction *mAppendSignature = nullptr; QAction *mPrependSignature = nullptr; QAction *mInsertSignatureAtCursorPosition = nullptr; QAction *mSelectAll = nullptr; QAction *mFindText = nullptr; QAction *mFindNextText = nullptr; QAction *mReplaceText = nullptr; QLabel *mStatusbarLabel = nullptr; QLabel *mCursorLineLabel = nullptr; QLabel *mCursorColumnLabel = nullptr; EncryptionState mEncryptionState; KToggleAction *mMarkupAction = nullptr; std::shared_ptr mExpiryChecker; bool mIsModified = false; KRecentFilesAction *mRecentAction = nullptr; KToggleAction *mWordWrapAction = nullptr; KToggleAction *mAutoSpellCheckingAction = nullptr; bool mLastSignActionState = false; std::shared_ptr mKeyCache; bool mLastEncryptActionState = false; }; diff --git a/server/editor/recipient.cpp b/server/editor/recipient.cpp index 26f25d0..5e7374a 100644 --- a/server/editor/recipient.cpp +++ b/server/editor/recipient.cpp @@ -1,140 +1,155 @@ /* SPDX-FileCopyrightText: 2010 Volker Krause Based in kmail/recipientseditor.h/cpp SPDX-FileCopyrightText: 2004 Cornelius Schumacher SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0 */ #include "recipient.h" #include +#include using namespace KPIM; class RecipientPrivate { public: RecipientPrivate(const QString &email, Recipient::Type type) : mType(type) , mEmail(email) { } Kleo::Action mEncryptionAction = Kleo::Impossible; Recipient::Type mType; QString mEmail; QString mName; GpgME::Key mKey; }; Recipient::Recipient(const QString &email, Recipient::Type type) : d(new RecipientPrivate(email, type)) { + setRawEmail(email); } Recipient::~Recipient() = default; void Recipient::setType(Type type) { d->mType = type; } Recipient::Type Recipient::type() const { return d->mType; } void Recipient::setEmail(const QString &email) { d->mEmail = email; } +void Recipient::setRawEmail(const QString &email) +{ + KMime::Types::Mailbox mbox; + mbox.from7BitString(email.toUtf8()); + if (mbox.hasAddress()) { + setEmail(mbox.addrSpec().asString()); + setName(mbox.name()); + } else { + setEmail(email); + setName({}); + } +} + QString Recipient::email() const { return d->mEmail; } void Recipient::setName(const QString &name) { d->mName = name; } QString Recipient::name() const { return d->mName; } bool Recipient::isEmpty() const { return d->mEmail.isEmpty(); } void Recipient::clear() { d->mEmail.clear(); d->mType = Recipient::To; } int Recipient::typeToId(Recipient::Type type) { return static_cast(type); } Recipient::Type Recipient::idToType(int id) { return static_cast(id); } QString Recipient::typeLabel() const { return typeLabel(d->mType); } QString Recipient::typeLabel(Recipient::Type type) { switch (type) { case To: return i18nc("@label:listbox Recipient of an email message.", "To"); case Cc: return i18nc("@label:listbox Carbon Copy recipient of an email message.", "CC"); case Bcc: return i18nc("@label:listbox Blind carbon copy recipient of an email message.", "BCC"); case ReplyTo: return i18nc("@label:listbox Reply-To recipient of an email message.", "Reply-To"); case Undefined: break; } return xi18nc("@label:listbox", "Undefined Recipient Type"); } QStringList Recipient::allTypeLabels() { QStringList types; types.append(typeLabel(To)); types.append(typeLabel(Cc)); types.append(typeLabel(Bcc)); types.append(typeLabel(ReplyTo)); return types; } GpgME::Key Recipient::key() const { return d->mKey; } void Recipient::setKey(const GpgME::Key &key) { d->mKey = key; } Kleo::Action Recipient::encryptionAction() const { return d->mEncryptionAction; } void Recipient::setEncryptionAction(const Kleo::Action action) { d->mEncryptionAction = action; } diff --git a/server/editor/recipient.h b/server/editor/recipient.h index 25fff00..9ada60c 100644 --- a/server/editor/recipient.h +++ b/server/editor/recipient.h @@ -1,67 +1,70 @@ /* SPDX-FileCopyrightText: 2010 Volker Krause Based in kmail/recipientseditor.h/cpp SPDX-FileCopyrightText: 2004 Cornelius Schumacher SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0 */ #pragma once #include #include #include #include #include /** Represents a mail recipient. */ class RecipientPrivate; /** * @brief The Recipient class */ class Recipient : public KPIM::MultiplyingLineData { public: using Ptr = QSharedPointer; using List = QList; enum Type { To, Cc, Bcc, ReplyTo, Undefined, }; Recipient(const QString &email = QString(), Type type = To); // krazy:exclude=explicit ~Recipient() override; void setType(Type type); [[nodiscard]] Type type() const; + /// Set email and name from unparsed mailbox text. + void setRawEmail(const QString &email); + void setEmail(const QString &email); [[nodiscard]] QString email() const; void setName(const QString &name); [[nodiscard]] QString name() const; [[nodiscard]] bool isEmpty() const override; void clear() override; [[nodiscard]] static int typeToId(Type type); [[nodiscard]] static Type idToType(int id); [[nodiscard]] QString typeLabel() const; [[nodiscard]] static QString typeLabel(Type type); [[nodiscard]] static QStringList allTypeLabels(); void setEncryptionAction(const Kleo::Action action); [[nodiscard]] Kleo::Action encryptionAction() const; void setKey(const GpgME::Key &key); [[nodiscard]] GpgME::Key key() const; private: std::unique_ptr const d; }; diff --git a/server/editor/recipientline.cpp b/server/editor/recipientline.cpp index 9adafc7..57486a7 100644 --- a/server/editor/recipientline.cpp +++ b/server/editor/recipientline.cpp @@ -1,291 +1,301 @@ /* SPDX-FileCopyrightText: 2010 Casey Link SPDX-FileCopyrightText: 2009-2010 Klaralvdalens Datakonsult AB, a KDAB Group company SPDX-License-Identifier: LGPL-2.0-or-later */ #include "recipientline.h" #include #include +#include #include #include #include using namespace KPIM; +using namespace Qt::Literals::StringLiterals; RecipientComboBox::RecipientComboBox(QWidget *parent) : QComboBox(parent) { } void RecipientComboBox::keyPressEvent(QKeyEvent *ev) { if (ev->key() == Qt::Key_Right) { Q_EMIT rightPressed(); } else { QComboBox::keyPressEvent(ev); } } RecipientLineEdit::RecipientLineEdit(QWidget *parent) : KLineEdit(parent) , mToolButton(new QToolButton(this)) { mToolButton->setVisible(false); mToolButton->setCursor(Qt::ArrowCursor); const int size = sizeHint().height() - 5; mToolButton->setFixedSize(size, size); int padding = (sizeHint().height() - size) / 2; mToolButton->move(2, padding); mToolButton->setStyleSheet(QStringLiteral("QToolButton { border: none; }")); connect(mToolButton, &QToolButton::clicked, this, &RecipientLineEdit::iconClicked); } void RecipientLineEdit::keyPressEvent(QKeyEvent *ev) { // Laurent Bug:280153 /*if ( ev->key() == Qt::Key_Backspace && text().isEmpty() ) { ev->accept(); Q_EMIT deleteMe(); } else */ if (ev->key() == Qt::Key_Left && cursorPosition() == 0 && !ev->modifiers().testFlag(Qt::ShiftModifier)) { // Shift would be pressed during selection Q_EMIT leftPressed(); } else if (ev->key() == Qt::Key_Right && cursorPosition() == text().length() && !ev->modifiers().testFlag(Qt::ShiftModifier)) { // Shift would be pressed during selection Q_EMIT rightPressed(); } else { KLineEdit::keyPressEvent(ev); } } void RecipientLineEdit::setIcon(const QIcon &icon, const QString &tooltip) { if (icon.isNull()) { mToolButton->setVisible(false); setStyleSheet(QString()); } else { mToolButton->setIcon(icon); mToolButton->setToolTip(tooltip); const int padding = mToolButton->width() - style()->pixelMetric(QStyle::PM_DefaultFrameWidth); setStyleSheet(QStringLiteral("QLineEdit { padding-left: %1px }").arg(padding)); mToolButton->setVisible(true); } } RecipientLineNG::RecipientLineNG(QWidget *parent) : MultiplyingLine(parent) , mData(new Recipient) { setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); auto topLayout = new QHBoxLayout(this); topLayout->setContentsMargins({}); const QStringList recipientTypes = Recipient::allTypeLabels(); mCombo = new RecipientComboBox(this); mCombo->addItems(recipientTypes); topLayout->addWidget(mCombo); mCombo->setToolTip(i18nc("@label:listbox", "Select type of recipient")); mEdit = new RecipientLineEdit(this); mEdit->setToolTip(i18n("Set the list of email addresses to receive this message")); mEdit->setClearButtonEnabled(true); topLayout->addWidget(mEdit); mEdit->installEventFilter(this); connect(mEdit, &RecipientLineEdit::returnPressed, this, &RecipientLineNG::slotReturnPressed); connect(mEdit, &RecipientLineEdit::deleteMe, this, &RecipientLineNG::slotPropagateDeletion); connect(mEdit, &QLineEdit::textChanged, this, &RecipientLineNG::analyzeLine); connect(mEdit, &RecipientLineEdit::focusUp, this, &RecipientLineNG::slotFocusUp); connect(mEdit, &RecipientLineEdit::focusDown, this, &RecipientLineNG::slotFocusDown); connect(mEdit, &RecipientLineEdit::rightPressed, this, &RecipientLineNG::rightPressed); connect(mEdit, &RecipientLineEdit::iconClicked, this, &RecipientLineNG::iconClicked); connect(mEdit, &RecipientLineEdit::leftPressed, mCombo, qOverload<>(&QWidget::setFocus)); connect(mEdit, &RecipientLineEdit::editingFinished, this, &RecipientLineNG::slotEditingFinished); connect(mEdit, &RecipientLineEdit::clearButtonClicked, this, &RecipientLineNG::slotPropagateDeletion); connect(mCombo, &RecipientComboBox::rightPressed, mEdit, qOverload<>(&QWidget::setFocus)); connect(mCombo, &RecipientComboBox::activated, this, &RecipientLineNG::slotTypeModified); connect(mEdit, &RecipientLineEdit::addAddress, this, &RecipientLineNG::slotAddRecipient); } void RecipientLineNG::slotEditingFinished() { if (mEdit->text().isEmpty()) { Q_EMIT deleteLine(this); } } void RecipientLineNG::slotAddRecipient(const QString &email) { Q_EMIT addRecipient(this, email); slotReturnPressed(); } void RecipientLineNG::slotTypeModified() { mModified = true; Q_EMIT typeModified(this); } void RecipientLineNG::analyzeLine(const QString &text) { const QStringList r = KEmailAddress::splitAddressList(text); mRecipientsCount = r.count(); mModified = true; Q_EMIT countChanged(); } int RecipientLineNG::recipientsCount() const { return mRecipientsCount; } void RecipientLineNG::setData(const MultiplyingLineData::Ptr &data) { Recipient::Ptr rec = qSharedPointerDynamicCast(data); if (rec.isNull()) { return; } mData = rec; fieldsFromData(); } MultiplyingLineData::Ptr RecipientLineNG::data() const { if (isModified()) { const_cast(this)->dataFromFields(); } return mData; } void RecipientLineNG::dataFromFields() { if (!mData) { return; } const QString editStr(mEdit->text()); QString displayName; QString addrSpec; QString comment; if (KEmailAddress::splitAddress(editStr, displayName, addrSpec, comment) == KEmailAddress::AddressOk) { mData->setName(displayName); } - mData->setEmail(editStr); + + KMime::Types::Mailbox mbox; + mbox.from7BitString(editStr.toUtf8()); + if (mbox.hasAddress()) { + mData->setEmail(mbox.addrSpec().asString()); + mData->setName(mbox.name()); + } else { + mData->setEmail(editStr); + } mData->setType(Recipient::idToType(mCombo->currentIndex())); mModified = false; } void RecipientLineNG::fieldsFromData() { if (!mData) { return; } mCombo->setCurrentIndex(Recipient::typeToId(mData->type())); - mEdit->setText(mData->email()); + mEdit->setText(mData->name().isEmpty() ? mData->email() : mData->name() + u" <"_s +mData->email() + u'>'); } void RecipientLineNG::activate() { mEdit->setFocus(); } bool RecipientLineNG::isActive() const { return mEdit->hasFocus(); } bool RecipientLineNG::isEmpty() const { return mEdit->text().isEmpty(); } bool RecipientLineNG::isModified() const { return mModified || mEdit->isModified(); } void RecipientLineNG::clearModified() { mModified = false; mEdit->setModified(false); } int RecipientLineNG::setColumnWidth(int w) { w = qMax(w, mCombo->sizeHint().width()); mCombo->setFixedWidth(w); mCombo->updateGeometry(); parentWidget()->updateGeometry(); return w; } void RecipientLineNG::fixTabOrder(QWidget *previous) { setTabOrder(previous, mCombo); setTabOrder(mCombo, mEdit); } QWidget *RecipientLineNG::tabOut() const { return mEdit; } void RecipientLineNG::clear() { mRecipientsCount = 0; mEdit->clear(); } bool RecipientLineNG::canDeleteLineEdit() const { return true; } void RecipientLineNG::setCompletionMode(KCompletion::CompletionMode mode) { mEdit->setCompletionMode(mode); } Recipient::Type RecipientLineNG::recipientType() const { return Recipient::idToType(mCombo->currentIndex()); } void RecipientLineNG::setRecipientType(Recipient::Type type) { mCombo->setCurrentIndex(Recipient::typeToId(type)); slotTypeModified(); } Recipient::Ptr RecipientLineNG::recipient() const { return qSharedPointerDynamicCast(data()); } void RecipientLineNG::setIcon(const QIcon &icon, const QString &tooltip) { mEdit->setIcon(icon, tooltip); } QString RecipientLineNG::rawData() const { return mEdit->text(); } bool RecipientLineNG::eventFilter(QObject *watched, QEvent *event) { if (watched == mEdit) { if (event->type() == QEvent::FocusIn || event->type() == QEvent::FocusOut) { Q_EMIT activeChanged(); } } return false; } diff --git a/server/editor/recipientseditor.cpp b/server/editor/recipientseditor.cpp index 457f54c..d418efa 100644 --- a/server/editor/recipientseditor.cpp +++ b/server/editor/recipientseditor.cpp @@ -1,306 +1,310 @@ /* SPDX-FileCopyrightText: 2010 Casey Link SPDX-FileCopyrightText: 2009-2010 Klaralvdalens Datakonsult AB, a KDAB Group company Refactored from earlier code by: SPDX-FileCopyrightText: 2010 Volker Krause SPDX-FileCopyrightText: 2004 Cornelius Schumacher SPDX-License-Identifier: LGPL-2.0-or-later */ #include "recipientseditor.h" #include "recipient.h" #include #include #include #include #include #include #include using namespace KPIM; constexpr int gMaximumRecipients = 100; RecipientLineFactory::RecipientLineFactory(QObject *parent) : KPIM::MultiplyingLineFactory(parent) { } KPIM::MultiplyingLine *RecipientLineFactory::newLine(QWidget *p) { auto line = new RecipientLineNG(p); if (qobject_cast(parent())) { connect(line, &RecipientLineNG::addRecipient, qobject_cast(parent()), &RecipientsEditor::addRecipientSlot); } else { qWarning() << "RecipientLineFactory::newLine: We can't connect to new line" << parent(); } return line; } int RecipientLineFactory::maximumRecipients() { return gMaximumRecipients; } class RecipientsEditorPrivate { public: RecipientsEditorPrivate() = default; KConfig *mRecentAddressConfig = nullptr; bool mSkipTotal = false; }; RecipientsEditor::RecipientsEditor(QWidget *parent) : RecipientsEditor(new RecipientLineFactory(nullptr), parent) { } RecipientsEditor::RecipientsEditor(RecipientLineFactory *lineFactory, QWidget *parent) : MultiplyingLineEditor(lineFactory, parent) , d(new RecipientsEditorPrivate) { factory()->setParent(this); // HACK: can't use 'this' above since it's not yet constructed at that point // Install global event filter and listen for keypress events for RecipientLineEdits. // Unfortunately we can't install ourselves directly as event filter for the edits, // because the RecipientLineEdit has its own event filter installed into QApplication // and so it would eat the event before it would reach us. qApp->installEventFilter(this); connect(this, &RecipientsEditor::lineAdded, this, &RecipientsEditor::slotLineAdded); connect(this, &RecipientsEditor::lineDeleted, this, &RecipientsEditor::slotLineDeleted); addData(); // one default line } RecipientsEditor::~RecipientsEditor() = default; bool RecipientsEditor::addRecipient(const QString &recipient, Recipient::Type type) { return addData(Recipient::Ptr(new Recipient(recipient, type)), false); } void RecipientsEditor::addRecipientSlot(RecipientLineNG *line, const QString &recipient) { addRecipient(recipient, line->recipientType()); } bool RecipientsEditor::setRecipientString(const QList &mailboxes, Recipient::Type type) { int count = 1; for (const KMime::Types::Mailbox &mailbox : mailboxes) { if (count++ > gMaximumRecipients) { KMessageBox::error(this, i18ncp("@info:status", "Truncating recipients list to %2 of %1 entry.", "Truncating recipients list to %2 of %1 entries.", mailboxes.count(), gMaximumRecipients)); return true; } // Too many if (addRecipient(mailbox.prettyAddress(KMime::Types::Mailbox::QuoteWhenNecessary), type)) { return true; } } return false; } Recipient::List RecipientsEditor::recipients() const { const QList dataList = allData(); Recipient::List recList; for (const MultiplyingLineData::Ptr &datum : dataList) { Recipient::Ptr rec = qSharedPointerDynamicCast(datum); if (rec.isNull()) { continue; } recList << rec; } return recList; } Recipient::Ptr RecipientsEditor::activeRecipient() const { return qSharedPointerDynamicCast(activeData()); } QString RecipientsEditor::recipientString(Recipient::Type type) const { return recipientStringList(type).join(QLatin1String(", ")); } QStringList RecipientsEditor::recipientStringList(Recipient::Type type) const { QStringList selectedRecipients; for (const Recipient::Ptr &r : recipients()) { if (r->type() == type) { - selectedRecipients << r->email(); + if (r->name().isEmpty()) { + selectedRecipients << r->email(); + } else { + selectedRecipients << r->name() + u" <" + r->email() + u'>'; + } } } return selectedRecipients; } void RecipientsEditor::removeRecipient(const QString &recipient, Recipient::Type type) { // search a line which matches recipient and type QListIterator it(lines()); MultiplyingLine *line = nullptr; while (it.hasNext()) { line = it.next(); auto rec = qobject_cast(line); if (rec) { if ((rec->recipient()->email() == recipient) && (rec->recipientType() == type)) { break; } } } if (line) { line->slotPropagateDeletion(); } } void RecipientsEditor::slotLineAdded(MultiplyingLine *line) { // subtract 1 here, because we want the number of lines // before this line was added. const int count = lines().size() - 1; auto rec = qobject_cast(line); if (!rec) { return; } if (count > 0) { if (count == 1) { rec->setRecipientType(Recipient::To); } else { auto last_rec = qobject_cast(lines().at(lines().count() - 2)); if (last_rec) { if (last_rec->recipientType() == Recipient::ReplyTo) { rec->setRecipientType(Recipient::To); } else { rec->setRecipientType(last_rec->recipientType()); } } } line->fixTabOrder(lines().constLast()->tabOut()); } connect(rec, &RecipientLineNG::countChanged, this, &RecipientsEditor::slotCalculateTotal); } void RecipientsEditor::slotLineDeleted(int pos) { bool atLeastOneToLine = false; int firstCC = -1; for (int i = pos, total = lines().count(); i < total; ++i) { MultiplyingLine *line = lines().at(i); auto rec = qobject_cast(line); if (rec) { if (rec->recipientType() == Recipient::To) { atLeastOneToLine = true; } else if ((rec->recipientType() == Recipient::Cc) && (firstCC < 0)) { firstCC = i; } } } if (!atLeastOneToLine && (firstCC >= 0)) { auto firstCCLine = qobject_cast(lines().at(firstCC)); if (firstCCLine) { firstCCLine->setRecipientType(Recipient::To); } } slotCalculateTotal(); } bool RecipientsEditor::eventFilter(QObject *object, QEvent *event) { if (event->type() == QEvent::KeyPress && qobject_cast(object)) { auto ke = static_cast(event); // Treats comma or semicolon as email separator, will automatically move focus // to a new line, basically preventing user from inputting more than one // email address per line, which breaks our opportunistic crypto in composer if (ke->key() == Qt::Key_Comma || (ke->key() == Qt::Key_Semicolon)) { auto line = qobject_cast(object->parent()); const auto split = KEmailAddress::splitAddressList(line->rawData() + QLatin1String(", ")); if (split.size() > 1) { addRecipient(QString(), line->recipientType()); setFocusBottom(); return true; } } } else if (event->type() == QEvent::FocusIn && qobject_cast(object)) { Q_EMIT focusInRecipientLineEdit(); } return false; } void RecipientsEditor::slotCalculateTotal() { // Prevent endless recursion when splitting recipient if (d->mSkipTotal) { return; } int empty = 0; const auto currentLines = lines(); for (auto line : currentLines) { auto rec = qobject_cast(line); if (rec) { if (rec->isEmpty()) { ++empty; } else { const int recipientsCount = rec->recipientsCount(); if (recipientsCount > 1) { // Ensure we always have only one recipient per line d->mSkipTotal = true; Recipient::Ptr recipient = rec->recipient(); const auto split = KEmailAddress::splitAddressList(recipient->email()); bool maximumElementFound = false; for (int i = 1 /* sic! */; i < split.count(); ++i) { maximumElementFound = addRecipient(split[i], rec->recipientType()); if (maximumElementFound) { break; } } - recipient->setEmail(split[0]); + recipient->setRawEmail(split[0]); rec->setData(recipient); setFocusBottom(); // focus next empty entry d->mSkipTotal = false; if (maximumElementFound) { return; } } } } } // We always want at least one empty line if (empty == 0) { addData({}, false); } int count = 0; const auto linesP{lines()}; for (auto line : linesP) { auto rec = qobject_cast(line); if (rec) { if (!rec->isEmpty()) { count++; } } } } RecipientLineNG *RecipientsEditor::activeLine() const { MultiplyingLine *line = MultiplyingLineEditor::activeLine(); return qobject_cast(line); } diff --git a/server/main.cpp b/server/main.cpp index 675e011..e56061a 100644 --- a/server/main.cpp +++ b/server/main.cpp @@ -1,52 +1,41 @@ // SPDX-FileCopyrightText: 2023 g10 code Gmbh // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "controllers/emailcontroller.h" #include #include #include #include #include #include #include #include #include #include "websocketclient.h" +#include "webserver.h" using namespace Qt::Literals::StringLiterals; using namespace std::chrono; int main(int argc, char *argv[]) { QApplication app(argc, argv); app.setQuitOnLastWindowClosed(false); KLocalizedString::setApplicationDomain(QByteArrayLiteral("gpgol")); - QHttpServer server; - - server.route(u"/view"_s, &EmailController::viewEmailAction); - server.route(u"/info"_s, &EmailController::infoEmailAction); - server.route(u"/new"_s, &EmailController::newEmailAction); - server.route(u"/forward"_s, &EmailController::forwardEmailAction); - server.route(u"/reply"_s, &EmailController::replyEmailAction); - server.route(u"/draft/"_s, &EmailController::draftAction); - - server.afterRequest([](QHttpServerResponse &&resp) { - resp.setHeader("Access-Control-Allow-Origin", "*"); - return std::move(resp); - }); - - const auto port = server.listen(); - if (!port) { + WebServer server; + server.run(); + if (!server.running()) { qWarning() << "Server failed to listen on a port."; return 1; } + const auto port = server.port(); qWarning() << u"Running on http://127.0.0.1:%1/ (Press CTRL+C to quit)"_s.arg(port); - auto &websocketClient = WebsocketClient::self(QUrl(u"wss://127.0.0.1:5657"_s), port); + WebsocketClient::self(QUrl(u"wss://127.0.0.1:5657"_s), port); return app.exec(); } diff --git a/server/webserver.cpp b/server/webserver.cpp new file mode 100644 index 0000000..b67066a --- /dev/null +++ b/server/webserver.cpp @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2024 g10 code Gmbh +// SPDX-Contributor: Carl Schwan +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "webserver.h" +#include "controllers/emailcontroller.h" + +using namespace Qt::Literals::StringLiterals; + +WebServer::WebServer(QObject *parent) + : QObject(parent) +{ + m_server.route(u"/view"_s, &EmailController::viewEmailAction); + m_server.route(u"/info"_s, &EmailController::infoEmailAction); + m_server.route(u"/new"_s, &EmailController::newEmailAction); + m_server.route(u"/forward"_s, &EmailController::forwardEmailAction); + m_server.route(u"/reply"_s, &EmailController::replyEmailAction); + m_server.route(u"/draft/"_s, &EmailController::draftAction); + + m_server.afterRequest([](QHttpServerResponse &&resp) { + resp.setHeader("Access-Control-Allow-Origin", "*"); + return std::move(resp); + }); +} + +void WebServer::run() +{ + m_port = m_server.listen(); + if (!m_port) { + qWarning() << "Server failed to listen on a port."; + m_running = false; + return; + } + qWarning() << u"Running on http://127.0.0.1:%1/ (Press CTRL+C to quit)"_s.arg(m_port); + m_running = true; +} + +int WebServer::port() const +{ + return m_port; +} + +bool WebServer::running() const +{ + return m_running; +} diff --git a/server/webserver.h b/server/webserver.h new file mode 100644 index 0000000..44aeab8 --- /dev/null +++ b/server/webserver.h @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2024 g10 code Gmbh +// SPDX-Contributor: Carl Schwan +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include + +/// The webserver of the native client +/// +/// This webserver will receive webrequests from the outlook web client via +/// the broker. +class WebServer : public QObject +{ + Q_OBJECT +public: + /// Default contructor. + WebServer(QObject *parent = nullptr); + + /// Start webserver. + void run(); + + /// \return the port of the webserver. Qt will randomly assign a free port to this + /// process. + int port() const; + + /// \return whether the webserver is running. + bool running() const; + +private: + int m_port; + bool m_running = false; + QHttpServer m_server; +};