diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 3469600..7e2d29b 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -1,287 +1,285 @@ # SPDX-FileCopyrightText: 2023 g10 code GmbH # SPDX-Contributor: Carl Schwan # SPDX-License-Identifier: BSD-2-Clause add_library(gpgol-server-static STATIC) target_sources(gpgol-server-static PRIVATE websocketclient.cpp websocketclient.h webserver.h webserver.cpp # Identity identity/addressvalidationjob.cpp identity/addressvalidationjob.h identity/identitymanager.cpp identity/identitymanager.h identity/identitydialog.cpp identity/identitydialog.h - identity/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/addresseelineedit.cpp editor/addresseelineedit.h editor/addresseelineeditmanager.cpp editor/addresseelineeditmanager.h editor/composer.cpp editor/composer.h editor/composerviewbase.cpp editor/composerviewbase.h editor/composerwindow.cpp editor/composerwindow.h editor/composerwindowfactory.cpp editor/composerwindowfactory.h editor/cryptostateindicatorwidget.cpp editor/cryptostateindicatorwidget.h editor/kmcomposerglobalaction.cpp editor/kmcomposerglobalaction.h editor/nearexpirywarning.cpp editor/nearexpirywarning.h editor/mailtemplates.cpp editor/mailtemplates.h editor/recipient.cpp editor/recipient.h editor/recipientline.cpp editor/recipientline.h editor/recipientseditor.cpp editor/recipientseditor.h editor/util.h editor/util.cpp editor/kmailcompletion.cpp editor/kmailcompletion.h editor/richtextcomposerng.cpp editor/richtextcomposerng.h editor/richtextcomposersignatures.cpp editor/richtextcomposersignatures.h editor/nodehelper.cpp editor/nodehelper.h editor/signaturecontroller.cpp editor/signaturecontroller.h editor/spellcheckerconfigdialog.cpp editor/spellcheckerconfigdialog.h # Editor job editor/job/abstractencryptjob.h editor/job/autocryptheadersjob.h editor/job/contentjobbase.h editor/job/contentjobbase_p.h editor/job/dndfromarkjob.cpp editor/job/dndfromarkjob.h editor/job/encryptjob.h editor/job/inserttextfilejob.h editor/job/itipjob.h editor/job/jobbase.h editor/job/jobbase_p.h editor/job/maintextjob.h editor/job/multipartjob.h editor/job/protectedheadersjob.h editor/job/signencryptjob.h editor/job/signjob.h editor/job/singlepartjob.h editor/job/skeletonmessagejob.h editor/job/transparentjob.h editor/job/autocryptheadersjob.cpp editor/job/contentjobbase.cpp editor/job/encryptjob.cpp editor/job/inserttextfilejob.cpp editor/job/itipjob.cpp editor/job/jobbase.cpp editor/job/maintextjob.cpp editor/job/multipartjob.cpp editor/job/protectedheadersjob.cpp editor/job/saveasfilejob.cpp editor/job/saveasfilejob.h editor/job/signencryptjob.cpp editor/job/signjob.cpp editor/job/singlepartjob.cpp editor/job/skeletonmessagejob.cpp editor/job/transparentjob.cpp ## Editor Part editor/part/globalpart.h editor/part/infopart.h editor/part/itippart.h editor/part/messagepart.h editor/part/textpart.h editor/part/globalpart.cpp editor/part/infopart.cpp editor/part/itippart.cpp editor/part/messagepart.cpp editor/part/textpart.cpp ## Attachment editor/attachment/attachmentjob.cpp editor/attachment/attachmentjob.h editor/attachment/attachmentclipboardjob.cpp editor/attachment/attachmentclipboardjob.h editor/attachment/attachmentcompressjob.cpp editor/attachment/attachmentcompressjob.h editor/attachment/attachmentcontroller.cpp editor/attachment/attachmentcontroller.h editor/attachment/attachmentcontrollerbase.cpp editor/attachment/attachmentcontrollerbase.h editor/attachment/attachmentfromfolderjob.cpp editor/attachment/attachmentfromfolderjob.h editor/attachment/attachmentfrommimecontentjob.cpp editor/attachment/attachmentfrommimecontentjob.h editor/attachment/attachmentfromurlbasejob.cpp editor/attachment/attachmentfromurlbasejob.h editor/attachment/attachmentfromurljob.cpp editor/attachment/attachmentfromurljob.h editor/attachment/attachmentfromurlutils.cpp editor/attachment/attachmentfromurlutils.h editor/attachment/attachmentfrompublickeyjob.cpp editor/attachment/attachmentfrompublickeyjob.h editor/attachment/attachmentloadjob.cpp editor/attachment/attachmentloadjob.h editor/attachment/attachmentmodel.cpp editor/attachment/attachmentmodel.h editor/attachment/attachmentpart.cpp editor/attachment/attachmentpart.h editor/attachment/attachmentpropertiesdialog.cpp editor/attachment/attachmentpropertiesdialog.h editor/attachment/attachmentupdatejob.cpp editor/attachment/attachmentupdatejob.h editor/attachment/attachmentview.cpp editor/attachment/attachmentview.h editor/attachment/editorwatcher.cpp editor/attachment/editorwatcher.h ) qt_add_resources(gpgol-server-static PREFIX "/" FILES assets/certificate.crt ) ki18n_wrap_ui(gpgol-server-static editor/attachment/ui/attachmentpropertiesdialog.ui editor/attachment/ui/attachmentpropertiesdialog_readonly.ui ) ecm_qt_declare_logging_category(gpgol-server-static_SRCS HEADER websocket_debug.h IDENTIFIER WEBSOCKET_LOG CATEGORY_NAME org.gpgol.server.websocket DESCRIPTION "Websocket connection in the server" EXPORT GPGOL ) ecm_qt_declare_logging_category(gpgol-server-static_SRCS HEADER ewsresource_debug.h IDENTIFIER EWSRES_LOG CATEGORY_NAME org.gpgol.ews DESCRIPTION "Ews mail client" EXPORT GPGOL ) ecm_qt_declare_logging_category(gpgol-server-static_SRCS HEADER ewscli_debug.h IDENTIFIER EWSCLI_LOG CATEGORY_NAME org.gpgol.ews.client DESCRIPTION "ews client (gpgol-server)" EXPORT GPGOL ) ecm_qt_declare_logging_category(gpgol-server-static_SRCS HEADER editor_debug.h IDENTIFIER EDITOR_LOG CATEGORY_NAME org.gpgol.editor DESCRIPTION "mail composer" EXPORT GPGOL ) set(WARN_TOOMANY_RECIPIENTS_DEFAULT true) set(ALLOW_SEMICOLON_AS_ADDRESS_SEPARATOR_DEFAULT true) configure_file(editor/settings/messagecomposer.kcfg.in ${CMAKE_CURRENT_BINARY_DIR}/messagecomposer.kcfg) kconfig_add_kcfg_files(gpgol-server-static editor/settings/messagecomposersettings.kcfgc) install(FILES composerui.rc DESTINATION ${KDE_INSTALL_KXMLGUIDIR}/gpgol-server) target_sources(gpgol-server-static PUBLIC ${gpgol-server-static_SRCS}) target_link_libraries(gpgol-server-static PUBLIC common Qt6::HttpServer Qt6::Widgets Qt6::PrintSupport KF6::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/editor/composerwindow.cpp b/server/editor/composerwindow.cpp index 4599612..b5c5491 100644 --- a/server/editor/composerwindow.cpp +++ b/server/editor/composerwindow.cpp @@ -1,1690 +1,1692 @@ // SPDX-FileCopyrightText: 2023 g10 code Gmbh // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "composerwindow.h" // Qt includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // KDE includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Gpgme includes #include #include // App includes #include "../identity/identitymanager.h" #include "../identity/identitydialog.h" #include "recipientseditor.h" #include "nearexpirywarning.h" #include "composerviewbase.h" #include "richtextcomposerng.h" #include "signaturecontroller.h" #include "job/dndfromarkjob.h" #include "job/saveasfilejob.h" #include "job/inserttextfilejob.h" #include "attachment/attachmentcontroller.h" #include "attachment/attachmentview.h" #include "attachment/attachmentmodel.h" #include "kmcomposerglobalaction.h" #include "mailtemplates.h" #include "messagecomposersettings.h" #include "spellcheckerconfigdialog.h" #include "websocketclient.h" #include "draft/draftmanager.h" using namespace Qt::Literals::StringLiterals; using namespace std::chrono_literals; namespace { inline bool containsSMIME(unsigned int f) { return f & (Kleo::SMIMEFormat | Kleo::SMIMEOpaqueFormat); } inline bool containsOpenPGP(unsigned int f) { return f & (Kleo::OpenPGPMIMEFormat | Kleo::InlineOpenPGPFormat); } auto findSendersUid(const std::string &addrSpec, const std::vector &userIds) { return std::find_if(userIds.cbegin(), userIds.cend(), [&addrSpec](const auto &uid) { return uid.addrSpec() == addrSpec || (uid.addrSpec().empty() && std::string(uid.email()) == addrSpec) || (uid.addrSpec().empty() && (!uid.email() || !*uid.email()) && uid.name() == addrSpec); }); } } ComposerWindow::ComposerWindow(const QString &from, const QString &name, const QByteArray &bearerToken, QWidget *parent) : KXmlGuiWindow(parent) , mFrom(from) , mMainWidget(new QWidget(this)) , mComposerBase(new MessageComposer::ComposerViewBase(this)) , mHeadersToEditorSplitter(new QSplitter(Qt::Vertical, mMainWidget)) , mHeadersArea(new QWidget(mHeadersToEditorSplitter)) , mGrid(new QGridLayout(mHeadersArea)) , mLblFrom(new QLabel(i18nc("sender address field", "From:"), mHeadersArea)) , mButtonFrom(new QPushButton(mHeadersArea)) , mRecipientEditor(new RecipientsEditor(mHeadersArea)) , mLblSubject(new QLabel(i18nc("@label:textbox Subject of email.", "Subject:"), mHeadersArea)) , mEdtSubject(new QLineEdit(mHeadersArea)) , mRichTextComposer(new MessageComposer::RichTextComposerNg(this)) , mRichTextEditorWidget(new TextCustomEditor::RichTextEditorWidget(mRichTextComposer, mMainWidget)) , mNearExpiryWarning(new NearExpiryWarning(this)) , mGlobalAction(new KMComposerGlobalAction(this, this)) , mKeyCache(Kleo::KeyCache::mutableInstance()) { connect(mKeyCache.get(), &Kleo::KeyCache::keyListingDone, this, [this](const GpgME::KeyListResult &result) { Q_UNUSED(result); mRunKeyResolverTimer->start(); }); bool isNew = false; mIdentity = IdentityManager::self().fromEmail(from, isNew); mEdtFrom = new QLabel(mHeadersArea); if (isNew) { // fill the idenity with default fields auto dlg = new KMail::IdentityDialog; mIdentity.setFullName(name); + mIdentity.setWarnNotEncrypt(true); + dlg->setIdentity(mIdentity); connect(dlg, &KMail::IdentityDialog::keyListingFinished, this, [this, dlg]() { dlg->updateIdentity(mIdentity); IdentityManager::self().updateIdentity(mIdentity); }); } mComposerBase->setBearerToken(bearerToken); mMainWidget->resize(800, 600); setCentralWidget(mMainWidget); setWindowTitle(i18nc("@title:window", "Composer")); setMinimumSize(200, 200); mHeadersToEditorSplitter->setObjectName(QStringLiteral("mHeadersToEditorSplitter")); mHeadersToEditorSplitter->setChildrenCollapsible(false); auto v = new QVBoxLayout(mMainWidget); v->setContentsMargins({}); v->addWidget(mNearExpiryWarning); v->addWidget(mHeadersToEditorSplitter); mHeadersArea->setSizePolicy(mHeadersToEditorSplitter->sizePolicy().horizontalPolicy(), QSizePolicy::Expanding); mHeadersToEditorSplitter->addWidget(mHeadersArea); const QList defaultSizes{0}; mHeadersToEditorSplitter->setSizes(defaultSizes); mGrid->setColumnStretch(0, 1); mGrid->setColumnStretch(1, 100); mGrid->setRowStretch(3 + 1, 100); int row = 0; mRunKeyResolverTimer = new QTimer(this); mRunKeyResolverTimer->setSingleShot(true); mRunKeyResolverTimer->setInterval(500ms); connect(mRunKeyResolverTimer, &QTimer::timeout, this, &ComposerWindow::runKeyResolver); // From mLblFrom->setObjectName(QStringLiteral("fromLineLabel")); mLblFrom->setFixedWidth(mRecipientEditor->setFirstColumnWidth(0)); mLblFrom->setBuddy(mEdtFrom); auto fromWrapper = new QWidget(mHeadersArea); auto fromWrapperLayout = new QHBoxLayout(fromWrapper); fromWrapperLayout->setContentsMargins({}); mEdtFrom->installEventFilter(this); mEdtFrom->setText(mFrom); mEdtFrom->setObjectName(QStringLiteral("fromLine")); fromWrapperLayout->addWidget(mEdtFrom); mComposerBase->setIdentity(mIdentity); mButtonFrom->setText(i18nc("@action:button", "Configure")); mButtonFrom->setIcon(QIcon::fromTheme(u"configure-symbolic"_s)); connect(mButtonFrom, &QPushButton::clicked, this, &ComposerWindow::slotEditIdentity); fromWrapperLayout->addWidget(mButtonFrom); mGrid->addWidget(mLblFrom, row, 0); mGrid->addWidget(fromWrapper, row, 1); row++; // Recipients mGrid->addWidget(mRecipientEditor, row, 0, 1, 2); mComposerBase->setRecipientsEditor(mRecipientEditor); mRecipientEditor->setCompletionMode(KCompletion::CompletionPopup); connect(mRecipientEditor, &RecipientsEditor::lineAdded, this, [this](KPIM::MultiplyingLine *line) { slotRecipientEditorLineAdded(qobject_cast(line)); }); row++; // Subject mEdtSubject->setObjectName(u"subjectLine"_s); mLblSubject->setObjectName(u"subjectLineLabel"_s); mLblSubject->setBuddy(mEdtSubject); mGrid->addWidget(mLblSubject, row, 0); mGrid->addWidget(mEdtSubject, row, 1); row++; auto editorWidget = new QWidget(); auto vLayout = new QVBoxLayout(editorWidget); vLayout->setContentsMargins({}); vLayout->setSpacing(0); mHeadersToEditorSplitter->addWidget(editorWidget); // Message widget auto connectionLossWidget = new KMessageWidget(this); connectionLossWidget->hide(); connectionLossWidget->setWordWrap(true); connectionLossWidget->setPosition(KMessageWidget::Position::Header); vLayout->addWidget(connectionLossWidget); auto &websocketClient = WebsocketClient::self(); connect(&websocketClient, &WebsocketClient::closed, this, [connectionLossWidget](const QString &errorMessage) { connectionLossWidget->setText(errorMessage); connectionLossWidget->show(); }); connect(&websocketClient, &WebsocketClient::connected, this, [connectionLossWidget]() { connectionLossWidget->hide(); }); connect(&websocketClient, &WebsocketClient::emailSentSuccessfully, this, [this](const QString &id) { if (id == mComposerBase->mailId()) { auto &draftManager = DraftManager::self(); draftManager.remove(draftManager.draftById(id.toUtf8())); hide(); } }); // Rich text editor mRichTextComposer->setProperty("_breeze_borders_sides", QVariant::fromValue(QFlags{Qt::TopEdge})); mRichTextComposer->setProperty("_breeze_force_frame", true); mComposerBase->setEditor(mRichTextComposer); vLayout->addWidget(mRichTextEditorWidget); auto attachmentModel = new MessageComposer::AttachmentModel(this); auto attachmentView = new AttachmentView(attachmentModel, mHeadersToEditorSplitter); attachmentView->hideIfEmpty(); connect(attachmentView, &AttachmentView::modified, this, &ComposerWindow::setModified); auto attachmentController = new AttachmentController(attachmentModel, attachmentView, this); mComposerBase->setAttachmentController(attachmentController); mComposerBase->setAttachmentModel(attachmentModel); auto signatureController = new MessageComposer::SignatureController(this); connect(signatureController, &MessageComposer::SignatureController::enableHtml, this, &ComposerWindow::enableHtml); signatureController->setIdentity(mIdentity); signatureController->setEditor(mComposerBase->editor()); mComposerBase->setSignatureController(signatureController); connect(signatureController, &MessageComposer::SignatureController::signatureAdded, mComposerBase->editor()->externalComposer(), &KPIMTextEdit::RichTextExternalComposer::startExternalEditor); setupStatusBar(attachmentView->widget()); setupActions(); setStandardToolBarMenuEnabled(true); toolBar(u"mainToolBar"_s)->show(); connect(expiryChecker().get(), &Kleo::ExpiryChecker::expiryMessage, this, [&](const GpgME::Key &key, QString msg, Kleo::ExpiryChecker::ExpiryInformation info, bool isNewMessage) { Q_UNUSED(isNewMessage); if (info == Kleo::ExpiryChecker::OwnKeyExpired || info == Kleo::ExpiryChecker::OwnKeyNearExpiry) { const auto plainMsg = msg.replace(QStringLiteral("

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

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

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

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

" "

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

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

" "Select the certificate(s) which should " "be used for this recipient. If there is no suitable certificate in the list " "you can also search for external certificates by clicking the button: " "search for external certificates.
", recipient->name().isEmpty() ? recipient->email() : recipient->name()); const bool opgp = containsOpenPGP(cryptoMessageFormat()); const bool x509 = containsSMIME(cryptoMessageFormat()); QPointer dlg = new Kleo::KeySelectionDialog( i18n("Encryption Key Selection"), msg, recipient->email(), {}, Kleo::KeySelectionDialog::ValidEncryptionKeys | (opgp ? Kleo::KeySelectionDialog::OpenPGPKeys : 0) | (x509 ? Kleo::KeySelectionDialog::SMIMEKeys : 0), false, // multi-selection false); // "remember choice" box; dlg->open(); connect(dlg, &QDialog::accepted, this, [dlg, recipient, line, this]() { auto key = dlg->selectedKey(); key.update(); // We need tofu information for key. recipient->setKey(key); annotateRecipientEditorLineWithCryptoInfo(line); }); } void ComposerWindow::slotRecipientEditorFocusChanged() { if (!mEncryptAction->isChecked()) { return; } if (mKeyCache->initialized()) { mRunKeyResolverTimer->stop(); runKeyResolver(); } } void ComposerWindow::slotRecipientAdded(RecipientLineNG *line) { if (line->recipientsCount() == 0) { return; } if (!mKeyCache->initialized()) { if (line->property("keyLookupJob").toBool()) { return; } line->setProperty("keyLookupJob", true); // We need to start key listing on our own othweise KMail will crash and we want to wait till the cache is populated. connect(mKeyCache.get(), &Kleo::KeyCache::keyListingDone, this, [this, line]() { slotRecipientAdded(line); }); return; } if (mKeyCache->initialized()) { mRunKeyResolverTimer->start(); } } void ComposerWindow::slotRecipientFocusLost(RecipientLineNG *line) { if (line->recipientsCount() == 0) { return; } if (mKeyCache->initialized()) { mRunKeyResolverTimer->start(); } } diff --git a/server/identity/identityaddvcarddialog.cpp b/server/identity/identityaddvcarddialog.cpp deleted file mode 100644 index 2bb9a33..0000000 --- a/server/identity/identityaddvcarddialog.cpp +++ /dev/null @@ -1,122 +0,0 @@ -/* - SPDX-FileCopyrightText: 2012-2023 Laurent Montel - - SPDX-License-Identifier: GPL-2.0-only -*/ - -#include "identityaddvcarddialog.h" - -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -IdentityAddVcardDialog::IdentityAddVcardDialog(const QStringList &shadowIdentities, QWidget *parent) - : QDialog(parent) - , mButtonGroup(new QButtonGroup(this)) - , mComboBox(new QComboBox(this)) - , mVCardPath(new KUrlRequester(this)) -{ - setWindowTitle(i18nc("@title:window", "Create own vCard")); - auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); - auto mainLayout = new QVBoxLayout(this); - QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); - okButton->setDefault(true); - okButton->setShortcut(Qt::CTRL | Qt::Key_Return); - connect(buttonBox, &QDialogButtonBox::accepted, this, &IdentityAddVcardDialog::accept); - connect(buttonBox, &QDialogButtonBox::rejected, this, &IdentityAddVcardDialog::reject); - setModal(true); - - auto mainWidget = new QWidget(this); - mainLayout->addWidget(mainWidget); - mainLayout->addWidget(buttonBox); - - auto vlay = new QVBoxLayout(mainWidget); - vlay->setContentsMargins({}); - - mButtonGroup->setObjectName(QStringLiteral("buttongroup")); - - // row 1: radio button - auto radio = new QRadioButton(i18n("&With empty fields"), this); - radio->setChecked(true); - vlay->addWidget(radio); - mButtonGroup->addButton(radio, static_cast(Empty)); - - // row 2: radio button - auto fromExistingVCard = new QRadioButton(i18n("&From existing vCard"), this); - vlay->addWidget(fromExistingVCard); - mButtonGroup->addButton(fromExistingVCard, static_cast(FromExistingVCard)); - - // row 3: KUrlRequester - auto hlay = new QHBoxLayout(); // inherits spacing - vlay->addLayout(hlay); - - mVCardPath->setObjectName(QStringLiteral("kurlrequester_vcardpath")); - mVCardPath->setMimeTypeFilters({QStringLiteral("text/vcard"), QStringLiteral("all/allfiles")}); - - mVCardPath->setMode(KFile::LocalOnly | KFile::File); - auto label = new QLabel(i18n("&vCard path:"), this); - label->setBuddy(mVCardPath); - label->setEnabled(false); - mVCardPath->setEnabled(false); - hlay->addWidget(label); - hlay->addWidget(mVCardPath); - - connect(fromExistingVCard, &QRadioButton::toggled, label, &QLabel::setEnabled); - connect(fromExistingVCard, &QRadioButton::toggled, mVCardPath, &KUrlRequester::setEnabled); - - // row 4: radio button - auto duplicateExistingVCard = new QRadioButton(i18n("&Duplicate existing vCard"), this); - vlay->addWidget(duplicateExistingVCard); - mButtonGroup->addButton(duplicateExistingVCard, static_cast(ExistingEntry)); - - // row 5: combobox with existing identities and label - hlay = new QHBoxLayout(); // inherits spacing - vlay->addLayout(hlay); - mComboBox->setObjectName(QStringLiteral("identity_combobox")); - mComboBox->setEditable(false); - - mComboBox->addItems(shadowIdentities); - mComboBox->setEnabled(false); - label = new QLabel(i18n("&Existing identities:"), this); - label->setBuddy(mComboBox); - label->setEnabled(false); - hlay->addWidget(label); - hlay->addWidget(mComboBox, 1); - - vlay->addWidget(new KSeparator); - vlay->addStretch(1); // spacer - - // enable/disable combobox and label depending on the third radio - // button's state: - connect(duplicateExistingVCard, &QRadioButton::toggled, label, &QLabel::setEnabled); - connect(duplicateExistingVCard, &QRadioButton::toggled, mComboBox, &QComboBox::setEnabled); - resize(350, 130); -} - -IdentityAddVcardDialog::~IdentityAddVcardDialog() = default; - -IdentityAddVcardDialog::DuplicateMode IdentityAddVcardDialog::duplicateMode() const -{ - const int id = mButtonGroup->checkedId(); - return static_cast(id); -} - -QString IdentityAddVcardDialog::duplicateVcardFromIdentity() const -{ - return mComboBox->currentText(); -} - -QUrl IdentityAddVcardDialog::existingVCard() const -{ - return mVCardPath->url(); -} - -#include "moc_identityaddvcarddialog.cpp" diff --git a/server/identity/identityaddvcarddialog.h b/server/identity/identityaddvcarddialog.h deleted file mode 100644 index 9c400da..0000000 --- a/server/identity/identityaddvcarddialog.h +++ /dev/null @@ -1,37 +0,0 @@ -/* - SPDX-FileCopyrightText: 2012-2023 Laurent Montel - - SPDX-License-Identifier: GPL-2.0-only -*/ - -#pragma once - -#include -#include - -class QButtonGroup; -class QComboBox; -class KUrlRequester; - -class IdentityAddVcardDialog : public QDialog -{ - Q_OBJECT -public: - enum DuplicateMode { - Empty, - ExistingEntry, - FromExistingVCard, - }; - - explicit IdentityAddVcardDialog(const QStringList &shadowIdentities, QWidget *parent = nullptr); - ~IdentityAddVcardDialog() override; - - [[nodiscard]] DuplicateMode duplicateMode() const; - [[nodiscard]] QString duplicateVcardFromIdentity() const; - [[nodiscard]] QUrl existingVCard() const; - -private: - QButtonGroup *const mButtonGroup; - QComboBox *const mComboBox; - KUrlRequester *const mVCardPath; -}; diff --git a/server/identity/identitydialog.cpp b/server/identity/identitydialog.cpp index c618897..c40e436 100644 --- a/server/identity/identitydialog.cpp +++ b/server/identity/identitydialog.cpp @@ -1,871 +1,627 @@ /* identitydialog.cpp This file is part of KMail, the KDE mail client. SPDX-FileCopyrightText: 2002 Marc Mutz SPDX-FileCopyrightText: 2014-2023 Laurent Montel SPDX-License-Identifier: GPL-2.0-only */ #include "identitydialog.h" #include "identitymanager.h" -#include "identityaddvcarddialog.h" #include "addressvalidationjob.h" #include "kleo_util.h" #include #include // other KMail headers: #include #include // other kdepim headers: #include #include #include // libkleopatra: #include #include #include #include // gpgme++ #include // other KDE headers: #include #include #include #include #include // Qt headers: #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // other headers: #include #include #include namespace KMail { class KeySelectionCombo : public Kleo::KeySelectionCombo { Q_OBJECT public: enum KeyType { SigningKey, EncryptionKey, }; explicit KeySelectionCombo(KeyType keyType, GpgME::Protocol protocol, QWidget *parent); ~KeySelectionCombo() override; void setIdentity(const QString &name, const QString &email); void init() override; private: void onCustomItemSelected(const QVariant &type); QString mEmail; QString mName; const KeyType mKeyType; const GpgME::Protocol mProtocol; }; class KeyGenerationJob : public QGpgME::Job { Q_OBJECT public: explicit KeyGenerationJob(const QString &name, const QString &email, KeySelectionCombo *parent); ~KeyGenerationJob() override; void slotCancel() override; void start(); private: void keyGenerated(const GpgME::KeyGenerationResult &result); const QString mName; const QString mEmail; QGpgME::Job *mJob = nullptr; }; KeyGenerationJob::KeyGenerationJob(const QString &name, const QString &email, KeySelectionCombo *parent) : QGpgME::Job(parent) , mName(name) , mEmail(email) { } KeyGenerationJob::~KeyGenerationJob() = default; void KeyGenerationJob::slotCancel() { if (mJob) { mJob->slotCancel(); } } void KeyGenerationJob::start() { auto job = new Kleo::DefaultKeyGenerationJob(this); connect(job, &Kleo::DefaultKeyGenerationJob::result, this, &KeyGenerationJob::keyGenerated); job->start(mEmail, mName); mJob = job; } void KeyGenerationJob::keyGenerated(const GpgME::KeyGenerationResult &result) { mJob = nullptr; if (result.error()) { KMessageBox::error(qobject_cast(parent()), i18n("Error while generating new key pair: %1", QString::fromUtf8(result.error().asString())), i18n("Key Generation Error")); Q_EMIT done(); return; } auto combo = qobject_cast(parent()); combo->setDefaultKey(QLatin1String(result.fingerprint())); connect(combo, &KeySelectionCombo::keyListingFinished, this, &KeyGenerationJob::done); combo->refreshKeys(); } KeySelectionCombo::KeySelectionCombo(KeyType keyType, GpgME::Protocol protocol, QWidget *parent) : Kleo::KeySelectionCombo(parent) , mKeyType(keyType) , mProtocol(protocol) { } KeySelectionCombo::~KeySelectionCombo() = default; void KeySelectionCombo::setIdentity(const QString &name, const QString &email) { mName = name; mEmail = email; setIdFilter(email); } void KeySelectionCombo::init() { Kleo::KeySelectionCombo::init(); std::shared_ptr keyFilter(new Kleo::DefaultKeyFilter); keyFilter->setIsOpenPGP(mProtocol == GpgME::OpenPGP ? Kleo::DefaultKeyFilter::Set : Kleo::DefaultKeyFilter::NotSet); if (mKeyType == SigningKey) { keyFilter->setCanSign(Kleo::DefaultKeyFilter::Set); keyFilter->setHasSecret(Kleo::DefaultKeyFilter::Set); } else { keyFilter->setCanEncrypt(Kleo::DefaultKeyFilter::Set); } setKeyFilter(keyFilter); prependCustomItem(QIcon(), i18n("No key"), QStringLiteral("no-key")); if (mProtocol == GpgME::OpenPGP) { appendCustomItem(QIcon::fromTheme(QStringLiteral("password-generate")), i18n("Generate a new key pair"), QStringLiteral("generate-new-key")); } connect(this, &KeySelectionCombo::customItemSelected, this, &KeySelectionCombo::onCustomItemSelected); } void KeySelectionCombo::onCustomItemSelected(const QVariant &type) { if (type == QLatin1String("no-key")) { return; } else if (type == QLatin1String("generate-new-key")) { auto job = new KeyGenerationJob(mName, mEmail, this); auto dlg = new Kleo::ProgressDialog(job, i18n("Generating new key pair..."), parentWidget()); dlg->setModal(true); setEnabled(false); connect(job, &KeyGenerationJob::done, this, [this]() { setEnabled(true); }); job->start(); } } IdentityDialog::IdentityDialog(QWidget *parent) : QDialog(parent) { setWindowTitle(i18nc("@title:window", "Edit Identity")); auto mainLayout = new QVBoxLayout(this); + mainLayout->setContentsMargins({}); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); connect(buttonBox, &QDialogButtonBox::accepted, this, &IdentityDialog::slotAccepted); connect(buttonBox, &QDialogButtonBox::rejected, this, &IdentityDialog::reject); // // Tab Widget: General // auto page = new QWidget(this); mainLayout->addWidget(page); - mainLayout->addWidget(buttonBox); + auto buttonBoxLayout = new QVBoxLayout; + buttonBoxLayout->setContentsMargins(style()->pixelMetric(QStyle::PM_LayoutLeftMargin), + style()->pixelMetric(QStyle::PM_LayoutTopMargin), + style()->pixelMetric(QStyle::PM_LayoutRightMargin), + style()->pixelMetric(QStyle::PM_LayoutBottomMargin)); + buttonBoxLayout->addWidget(buttonBox); + mainLayout->addLayout(buttonBoxLayout); auto vlay = new QVBoxLayout(page); vlay->setContentsMargins({}); mTabWidget = new QTabWidget(page); + mTabWidget->tabBar()->setExpanding(true); + mTabWidget->setDocumentMode(true); mTabWidget->setObjectName(QStringLiteral("config-identity-tab")); vlay->addWidget(mTabWidget); auto tab = new QWidget(mTabWidget); mTabWidget->addTab(tab, i18nc("@title:tab General identity settings.", "General")); auto formLayout = new QFormLayout(tab); // "Name" line edit and label: mNameEdit = new QLineEdit(tab); KLineEditEventHandler::catchReturnKey(mNameEdit); auto label = new QLabel(i18n("&Your name:"), tab); formLayout->addRow(label, mNameEdit); label->setBuddy(mNameEdit); QString msg = i18n( "

Your name

" "

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

" "

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

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

Organization

" "

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

" "

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

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

Email address

" "

This field should have your full email address.

" "

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

" "

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

"); label->setWhatsThis(msg); mEmailEdit->setWhatsThis(msg); auto emailValidator = new KEmailValidator(this); mEmailEdit->setValidator(emailValidator); - // "Email Aliases" string text edit and label: - mAliasEdit = new KEditListWidget(tab); - - auto emailValidator1 = new KEmailValidator(this); - mAliasEdit->lineEdit()->setValidator(emailValidator1); - - label = new QLabel(i18n("Email a&liases:"), tab); - formLayout->addRow(label, mAliasEdit); - label->setBuddy(mAliasEdit); - - msg = i18n( - "

Email aliases

" - "

This field contains alias addresses that should also " - "be considered as belonging to this identity (as opposed " - "to representing a different identity).

" - "

Example:

" - "" - "" - "" - "
Primary address:first.last@example.org
Aliases:first@example.org
last@example.org
" - "

Type one alias address per line.

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

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

" "

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

" "

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

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

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

" "

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

" "

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

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

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

" "

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

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

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

" "

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

"); label = new QLabel(i18n("S/MIME encryption certificate:"), mCryptographyTab); label->setBuddy(mSMIMEEncryptionKeyRequester); mSMIMEEncryptionKeyRequester->setWhatsThis(msg); connect(mSMIMEEncryptionKeyRequester, &KeySelectionCombo::keyListingFinished, this, [this] { slotKeyListingFinished(mSMIMEEncryptionKeyRequester); }); label->setWhatsThis(msg); formLayout->addRow(label, mSMIMEEncryptionKeyRequester); label->setEnabled(smimeProtocol); mSMIMEEncryptionKeyRequester->setEnabled(smimeProtocol); // "Preferred Crypto Message Format" combobox and label: mPreferredCryptoMessageFormat = new QComboBox(mCryptographyTab); QStringList l; l << Kleo::cryptoMessageFormatToLabel(Kleo::AutoFormat) << Kleo::cryptoMessageFormatToLabel(Kleo::InlineOpenPGPFormat) << Kleo::cryptoMessageFormatToLabel(Kleo::OpenPGPMIMEFormat) << Kleo::cryptoMessageFormatToLabel(Kleo::SMIMEFormat) << Kleo::cryptoMessageFormatToLabel(Kleo::SMIMEOpaqueFormat); mPreferredCryptoMessageFormat->addItems(l); label = new QLabel(i18nc("preferred format of encrypted messages", "Preferred format:"), mCryptographyTab); label->setBuddy(mPreferredCryptoMessageFormat); formLayout->addRow(label, mPreferredCryptoMessageFormat); - auto wrapper = new QWidget; - formLayout->addRow(i18nc("@label", "Security:"), wrapper); - - vlay = new QVBoxLayout(wrapper); - - mAutoSign = new QCheckBox(i18n("Sign messages")); - vlay->addWidget(mAutoSign); - - mAutoEncrypt = new QCheckBox(i18n("Encrypt messages when possible")); - vlay->addWidget(mAutoEncrypt); - - mWarnNotSign = new QCheckBox(i18n("Warn when trying to send unsigned messages")); - vlay->addWidget(mWarnNotSign); - mWarnNotEncrypt = new QCheckBox(i18n("Warn when trying to send unencrypted messages")); - vlay->addWidget(mWarnNotEncrypt); - - // - // Tab Widget: Advanced - // - tab = new QWidget(mTabWidget); - auto advancedMainLayout = new QVBoxLayout(tab); - mTabWidget->addTab(tab, i18nc("@title:tab Advanced identity settings.", "Advanced")); - formLayout = new QFormLayout; - advancedMainLayout->addLayout(formLayout); - - // "Reply-To Address" line edit and label: - mReplyToEdit = new QLineEdit(tab); - mReplyToEdit->setClearButtonEnabled(true); - mReplyToEdit->setObjectName(QStringLiteral("mReplyToEdit")); - label = new QLabel(i18n("&Reply-To address:"), tab); - label->setBuddy(mReplyToEdit); - formLayout->addRow(label, mReplyToEdit); - msg = i18n( - "

Reply-To addresses

" - "

This sets the Reply-to: header to contain a " - "different email address to the normal From: " - "address.

" - "

This can be useful when you have a group of people " - "working together in similar roles. For example, you " - "might want any emails sent to have your email in the " - "From: field, but any responses to go to " - "a group address.

" - "

If in doubt, leave this field blank.

"); - label->setWhatsThis(msg); - mReplyToEdit->setWhatsThis(msg); - KLineEditEventHandler::catchReturnKey(mReplyToEdit); - - // "Dictionary" combo box and label: - mDictionaryCombo = new Sonnet::DictionaryComboBox(tab); - label = new QLabel(i18n("D&ictionary:"), tab); - label->setBuddy(mDictionaryCombo); - formLayout->addRow(label, mDictionaryCombo); - - mAttachMyVCard = new QCheckBox(i18n("Attach my vCard to message"), tab); - mEditVCard = new QPushButton(i18n("Create..."), tab); - connect(mEditVCard, &QPushButton::clicked, this, &IdentityDialog::slotEditVcard); - formLayout->addRow(mAttachMyVCard, mEditVCard); - - // "default domain" input field: - auto hbox = new QHBoxLayout; - mDefaultDomainEdit = new QLineEdit(tab); - KLineEditEventHandler::catchReturnKey(mDefaultDomainEdit); - mDefaultDomainEdit->setClearButtonEnabled(true); - hbox->addWidget(mDefaultDomainEdit); - auto restoreDefaultDomainName = new QToolButton; - restoreDefaultDomainName->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh"))); - restoreDefaultDomainName->setToolTip(i18n("Restore default domain name")); - hbox->addWidget(restoreDefaultDomainName); - connect(restoreDefaultDomainName, &QToolButton::clicked, this, &IdentityDialog::slotRefreshDefaultDomainName); - label = new QLabel(i18n("Defaul&t domain:"), tab); - label->setBuddy(mDefaultDomainEdit); - formLayout->addRow(label, mDefaultDomainEdit); - - // and now: add QWhatsThis: - msg = i18n( - "

The default domain is used to complete email " - "addresses that only consist of the user's name." - "

"); - label->setWhatsThis(msg); - mDefaultDomainEdit->setWhatsThis(msg); - - // the last row is a spacer + formLayout->addRow(QString(), mWarnNotEncrypt); // // Tab Widget: Signature // mSignatureConfigurator = new KIdentityManagementWidgets::SignatureConfigurator(mTabWidget); mTabWidget->addTab(mSignatureConfigurator, i18n("Signature")); } IdentityDialog::~IdentityDialog() = default; -void IdentityDialog::slotRefreshDefaultDomainName() -{ - mDefaultDomainEdit->setText(QHostInfo::localHostName()); -} - void IdentityDialog::slotAccepted() { - const QStringList aliases = mAliasEdit->items(); - for (const QString &alias : aliases) { - if (alias.trimmed().isEmpty()) { - continue; - } - if (!KEmailAddress::isValidSimpleAddress(alias)) { - const QString errorMsg(KEmailAddress::simpleEmailAddressErrorMsg()); - KMessageBox::error(this, errorMsg, i18n("Invalid Email Alias \"%1\"", alias)); - return; - } - } - // Validate email addresses const QString email = mEmailEdit->text().trimmed(); if (email.isEmpty()) { KMessageBox::error(this, i18n("You must provide an email for this identity."), i18nc("@title:window", "Empty Email Address")); return; } if (!KEmailAddress::isValidSimpleAddress(email)) { const QString errorMsg(KEmailAddress::simpleEmailAddressErrorMsg()); KMessageBox::error(this, errorMsg, i18n("Invalid Email Address")); return; } - // Check if the 'Reply to' and 'BCC' recipients are valid - const QString recipients = mReplyToEdit->text().trimmed(); - auto job = new AddressValidationJob(recipients, this, this); - // Use default Value - job->setDefaultDomain(mDefaultDomainEdit->text()); - job->setProperty("email", email); - connect(job, &AddressValidationJob::result, this, &IdentityDialog::slotDelayedButtonClicked); - job->start(); -} - -bool IdentityDialog::keyMatchesEmailAddress(const GpgME::Key &key, const QString &email_) -{ - if (key.isNull()) { - return true; - } - const QString email = email_.trimmed().toLower(); - const auto uids = key.userIDs(); - for (const auto &uid : uids) { - QString em = QString::fromUtf8(uid.email() ? uid.email() : uid.id()); - if (em.isEmpty()) { - continue; - } - if (em[0] == QLatin1Char('<')) { - em = em.mid(1, em.length() - 2); - } - if (em.toLower() == email) { - return true; - } - } - - return false; -} - -void IdentityDialog::slotDelayedButtonClicked(KJob *job) -{ - const AddressValidationJob *validationJob = qobject_cast(job); - - // Abort if one of the recipient addresses is invalid - if (!validationJob->isValid()) { - return; - } - - const QString email = validationJob->property("email").toString(); - const GpgME::Key &pgpSigningKey = mPGPSigningKeyRequester->currentKey(); const GpgME::Key &pgpEncryptionKey = mPGPEncryptionKeyRequester->currentKey(); const GpgME::Key &smimeSigningKey = mSMIMESigningKeyRequester->currentKey(); const GpgME::Key &smimeEncryptionKey = mSMIMEEncryptionKeyRequester->currentKey(); QString msg; bool err = false; if (!keyMatchesEmailAddress(pgpSigningKey, email)) { msg = i18n( "One of the configured OpenPGP signing keys does not contain " "any user ID with the configured email address for this " "identity (%1).\n" "This might result in warning messages on the receiving side " "when trying to verify signatures made with this configuration.", email); err = true; } else if (!keyMatchesEmailAddress(pgpEncryptionKey, email)) { msg = i18n( "One of the configured OpenPGP encryption keys does not contain " "any user ID with the configured email address for this " "identity (%1).", email); err = true; } else if (!keyMatchesEmailAddress(smimeSigningKey, email)) { msg = i18n( "One of the configured S/MIME signing certificates does not contain " "the configured email address for this " "identity (%1).\n" "This might result in warning messages on the receiving side " "when trying to verify signatures made with this configuration.", email); err = true; } else if (!keyMatchesEmailAddress(smimeEncryptionKey, email)) { msg = i18n( "One of the configured S/MIME encryption certificates does not contain " "the configured email address for this " "identity (%1).", email); err = true; } if (err && KMessageBox::warningContinueCancel(this, msg, i18nc("@title:window", "Email Address Not Found in Key/Certificates"), KStandardGuiItem::cont(), KStandardGuiItem::cancel(), QStringLiteral("warn_email_not_in_certificate")) != KMessageBox::Continue) { return; } if (mSignatureConfigurator->isSignatureEnabled() && mSignatureConfigurator->signatureType() == Signature::FromFile) { QFileInfo file(mSignatureConfigurator->filePath()); if (!file.isReadable()) { KMessageBox::error(this, i18n("The signature file is not valid")); return; } } accept(); } +bool IdentityDialog::keyMatchesEmailAddress(const GpgME::Key &key, const QString &email_) +{ + if (key.isNull()) { + return true; + } + const QString email = email_.trimmed().toLower(); + const auto uids = key.userIDs(); + for (const auto &uid : uids) { + QString em = QString::fromUtf8(uid.email() ? uid.email() : uid.id()); + if (em.isEmpty()) { + continue; + } + if (em[0] == QLatin1Char('<')) { + em = em.mid(1, em.length() - 2); + } + if (em.toLower() == email) { + return true; + } + } + + return false; +} + void IdentityDialog::setIdentity(KIdentityManagementCore::Identity &ident) { setWindowTitle(i18nc("@title:window", "Edit Identity \"%1\"", ident.identityName())); // "General" tab: mNameEdit->setText(ident.fullName()); mOrganizationEdit->setText(ident.organization()); mEmailEdit->setText(ident.primaryEmailAddress()); - mAliasEdit->insertStringList(ident.emailAliases()); + mDictionaryCombo->setCurrentByDictionaryName(ident.dictionary()); // "Cryptography" tab: mPGPSigningKeyRequester->setDefaultKey(QLatin1String(ident.pgpSigningKey())); mPGPEncryptionKeyRequester->setDefaultKey(QLatin1String(ident.pgpEncryptionKey())); mPGPSameKey->setChecked(ident.pgpSigningKey() == ident.pgpEncryptionKey()); mSMIMESigningKeyRequester->setDefaultKey(QLatin1String(ident.smimeSigningKey())); mSMIMEEncryptionKeyRequester->setDefaultKey(QLatin1String(ident.smimeEncryptionKey())); mPreferredCryptoMessageFormat->setCurrentIndex(format2cb(Kleo::stringToCryptoMessageFormat(ident.preferredCryptoMessageFormat()))); - mAutoSign->setChecked(ident.pgpAutoSign()); - mAutoEncrypt->setChecked(ident.pgpAutoEncrypt()); - mWarnNotSign->setChecked(ident.warnNotSign()); mWarnNotEncrypt->setChecked(ident.warnNotEncrypt()); - // "Advanced" tab: - mReplyToEdit->setText(ident.replyToAddr()); - mDictionaryCombo->setCurrentByDictionaryName(ident.dictionary()); - - mVcardFilename = ident.vCardFile(); - - updateVcardButton(); - if (mVcardFilename.isEmpty()) { - mVcardFilename = - QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + QLatin1Char('/') + ident.identityName() + QLatin1String(".vcf"); - QFileInfo fileInfo(mVcardFilename); - QDir().mkpath(fileInfo.absolutePath()); - } else { - // Convert path. - const QString path = - QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + QLatin1Char('/') + ident.identityName() + QLatin1String(".vcf"); - if (QFileInfo::exists(path) && (mVcardFilename != path)) { - mVcardFilename = path; - } - } - mAttachMyVCard->setChecked(ident.attachVcard()); - QString defaultDomainName = ident.defaultDomainName(); - if (defaultDomainName.isEmpty()) { - defaultDomainName = QHostInfo::localHostName(); - } - mDefaultDomainEdit->setText(defaultDomainName); - // "Signature" tab: mSignatureConfigurator->setImageLocation(ident); mSignatureConfigurator->setSignature(ident.signature()); // set the configured email address as initial query of the key // requesters: const QString name = mNameEdit->text().trimmed(); const QString email = mEmailEdit->text().trimmed(); mPGPEncryptionKeyRequester->setIdentity(name, email); mPGPSigningKeyRequester->setIdentity(name, email); mSMIMEEncryptionKeyRequester->setIdentity(name, email); mSMIMESigningKeyRequester->setIdentity(name, email); } void IdentityDialog::updateIdentity(KIdentityManagementCore::Identity &ident) { // "General" tab: ident.setFullName(mNameEdit->text()); ident.setOrganization(mOrganizationEdit->text()); QString email = mEmailEdit->text().trimmed(); ident.setPrimaryEmailAddress(email); - const QStringList aliases = mAliasEdit->items(); - QStringList result; - for (const QString &alias : aliases) { - const QString aliasTrimmed = alias.trimmed(); - if (aliasTrimmed.isEmpty()) { - continue; - } - if (aliasTrimmed == email) { - continue; - } - result.append(alias); - } - ident.setEmailAliases(result); + // "Cryptography" tab: ident.setPGPSigningKey(mPGPSigningKeyRequester->currentKey().primaryFingerprint()); ident.setPGPEncryptionKey(mPGPEncryptionKeyRequester->currentKey().primaryFingerprint()); ident.setSMIMESigningKey(mSMIMESigningKeyRequester->currentKey().primaryFingerprint()); ident.setSMIMEEncryptionKey(mSMIMEEncryptionKeyRequester->currentKey().primaryFingerprint()); ident.setPreferredCryptoMessageFormat(QLatin1String(Kleo::cryptoMessageFormatToString(cb2format(mPreferredCryptoMessageFormat->currentIndex())))); ident.setEncryptionOverride(true); - ident.setPgpAutoSign(mAutoSign->isChecked()); - ident.setPgpAutoEncrypt(mAutoEncrypt->isChecked()); ident.setWarnNotEncrypt(mWarnNotEncrypt->isChecked()); ident.setWarnNotEncrypt(mWarnNotEncrypt->isChecked()); + // "Advanced" tab: - ident.setReplyToAddr(mReplyToEdit->text()); ident.setDictionary(mDictionaryCombo->currentDictionaryName()); - - ident.setVCardFile(mVcardFilename); - updateVcardButton(); - ident.setAttachVcard(mAttachMyVCard->isChecked()); - // Add default ? - ident.setDefaultDomainName(mDefaultDomainEdit->text()); - // "Signature" tab: ident.setSignature(mSignatureConfigurator->signature()); } -void IdentityDialog::slotEditVcard() -{ - auto &manager = IdentityManager::self(); - if (QFileInfo::exists(mVcardFilename)) { - editVcard(mVcardFilename); - } else { - QPointer dlg = new IdentityAddVcardDialog(manager.identityEmails(), this); - if (dlg->exec()) { - IdentityAddVcardDialog::DuplicateMode mode = dlg->duplicateMode(); - switch (mode) { - case IdentityAddVcardDialog::Empty: - editVcard(mVcardFilename); - break; - case IdentityAddVcardDialog::ExistingEntry: { - bool isNew = false; - KIdentityManagementCore::Identity ident = manager.fromEmail(dlg->duplicateVcardFromIdentity(), isNew); - const QString filename = ident.vCardFile(); - if (!filename.isEmpty()) { - QFile::copy(filename, mVcardFilename); - } - editVcard(mVcardFilename); - break; - } - case IdentityAddVcardDialog::FromExistingVCard: { - const QString filename = dlg->existingVCard().path(); - if (!filename.isEmpty()) { - mVcardFilename = filename; - } - editVcard(mVcardFilename); - break; - } - } - } - delete dlg; - } -} - -void IdentityDialog::editVcard(const QString &filename) -{ - /* - QPointer dlg = new IdentityEditVcardDialog(filename, this); - connect(dlg.data(), &IdentityEditVcardDialog::vcardRemoved, this, &IdentityDialog::slotVCardRemoved); - if (dlg->exec()) { - mVcardFilename = dlg->saveVcard(); - } - updateVcardButton(); - delete dlg; - */ -} - -void IdentityDialog::slotVCardRemoved() -{ - mVcardFilename.clear(); -} - -void IdentityDialog::updateVcardButton() -{ - if (mVcardFilename.isEmpty() || !QFileInfo::exists(mVcardFilename)) { - mEditVCard->setText(i18n("Create...")); - } else { - mEditVCard->setText(i18n("Edit...")); - } -} - void IdentityDialog::slotKeyListingFinished(KeySelectionCombo *combo) { mInitialLoadingFinished << combo; if (mInitialLoadingFinished.count() == 2) { Q_EMIT keyListingFinished(); } } } #include "identitydialog.moc" #include "moc_identitydialog.cpp" diff --git a/server/identity/identitydialog.h b/server/identity/identitydialog.h index 6141eaf..94a2553 100644 --- a/server/identity/identitydialog.h +++ b/server/identity/identitydialog.h @@ -1,107 +1,92 @@ /* identitydialog.h This file is part of KMail, the KDE mail client. SPDX-FileCopyrightText: 2002 Marc Mutz SPDX-License-Identifier: GPL-2.0-only */ #pragma once #include class QCheckBox; class KEditListWidget; class QComboBox; class QGroupBox; class KJob; class QLineEdit; class QPushButton; class QTabWidget; namespace GpgME { class Key; } namespace KIdentityManagementCore { class Identity; } namespace KIdentityManagementWidgets { class SignatureConfigurator; } namespace Sonnet { class DictionaryComboBox; } namespace KMail { class IdentityFolderRequester; class IdentityInvalidFolder; class KeySelectionCombo; class IdentityDialog : public QDialog { Q_OBJECT public: explicit IdentityDialog(QWidget *parent = nullptr); ~IdentityDialog() override; void setIdentity(/*_not_ const*/ KIdentityManagementCore::Identity &ident); void updateIdentity(KIdentityManagementCore::Identity &ident); Q_SIGNALS: void keyListingFinished(); private: // copy default templates to identity templates void slotAccepted(); - void slotDelayedButtonClicked(KJob *); - void slotEditVcard(); - void slotRefreshDefaultDomainName(); - void slotVCardRemoved(); void slotKeyListingFinished(KeySelectionCombo *combo); [[nodiscard]] bool keyMatchesEmailAddress(const GpgME::Key &key, const QString &email); [[nodiscard]] bool checkFolderExists(const QString &folder); - void updateVcardButton(); - void editVcard(const QString &filename); - QString mVcardFilename; QTabWidget *mTabWidget = nullptr; // "general" tab: QLineEdit *mNameEdit = nullptr; QLineEdit *mOrganizationEdit = nullptr; QLineEdit *mEmailEdit = nullptr; - KEditListWidget *mAliasEdit = nullptr; + Sonnet::DictionaryComboBox *mDictionaryCombo = nullptr; + // "cryptography" tab: QWidget *mCryptographyTab = nullptr; KeySelectionCombo *mPGPSigningKeyRequester = nullptr; KeySelectionCombo *mPGPEncryptionKeyRequester = nullptr; KeySelectionCombo *mSMIMESigningKeyRequester = nullptr; KeySelectionCombo *mSMIMEEncryptionKeyRequester = nullptr; QComboBox *mPreferredCryptoMessageFormat = nullptr; QCheckBox *mPGPSameKey = nullptr; - QCheckBox *mAutoSign = nullptr; - QCheckBox *mAutoEncrypt = nullptr; QCheckBox *mWarnNotEncrypt = nullptr; - QCheckBox *mWarnNotSign = nullptr; - // "advanced" tab: - QLineEdit *mReplyToEdit = nullptr; - Sonnet::DictionaryComboBox *mDictionaryCombo = nullptr; - QCheckBox *mAttachMyVCard = nullptr; - QPushButton *mEditVCard = nullptr; - QLineEdit *mDefaultDomainEdit = nullptr; QSet mInitialLoadingFinished; // "signature" tab: KIdentityManagementWidgets::SignatureConfigurator *mSignatureConfigurator = nullptr; }; } // namespace KMail diff --git a/server/identity/identitymanager.cpp b/server/identity/identitymanager.cpp index 32a6230..45a1288 100644 --- a/server/identity/identitymanager.cpp +++ b/server/identity/identitymanager.cpp @@ -1,132 +1,133 @@ // SPDX-FileCopyrightText: 2023 g10 code Gmbh // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "identitymanager.h" #include "editor_debug.h" #include #include #include IdentityManager::IdentityManager() : QObject(nullptr) { mConfig = new KConfig(QStringLiteral("emailidentities_gpgol")); if (!mConfig->isConfigWritable(true)) { qCWarning(EDITOR_LOG) << "impossible to write on this file"; } readConfig(mConfig); } IdentityManager::~IdentityManager() { writeConfig(); } IdentityManager &IdentityManager::self() { static IdentityManager _self; return _self; } void IdentityManager::updateIdentity(KIdentityManagementCore::Identity identity) { int i = 0; for (const auto &it : mIdentities) { if (it.uoid() == identity.uoid()) { mIdentities[i] = identity; return; } i++; } qCWarning(EDITOR_LOG) << "Identity not found"; mIdentities << identity; } KIdentityManagementCore::Identity IdentityManager::fromEmail(const QString &from, bool &isNew) { const auto it = std::find_if(std::cbegin(mIdentities), std::cend(mIdentities), [&from, &isNew](const auto &identity) { isNew = false; return identity.primaryEmailAddress() == from; }); if (it == std::cend(mIdentities)) { KIdentityManagementCore::Identity identity; identity.setPrimaryEmailAddress(from); + identity.setWarnNotEncrypt(true); mIdentities.append(identity); isNew = true; return identity; } return *it; } KIdentityManagementCore::Identity IdentityManager::fromUoid(uint uoid) { const auto it = std::find_if(std::cbegin(mIdentities), std::cend(mIdentities), [&uoid](const auto &identity) { return identity.uoid() == uoid; }); if (it == std::cend(mIdentities)) { qFatal() << "Should not happen"; } return *it; } QStringList IdentityManager::identityEmails() const { QStringList emails; std::transform(std::cbegin(mIdentities), std::cend(mIdentities), std::back_inserter(emails), [](const auto &identity) { return identity.primaryEmailAddress(); }); return emails; } KIdentityManagementCore::Identity::List IdentityManager::identities() const { return mIdentities; } void IdentityManager::writeConfig() const { const QStringList identities = groupList(mConfig); QStringList::const_iterator groupEnd = identities.constEnd(); for (QStringList::const_iterator group = identities.constBegin(); group != groupEnd; ++group) { mConfig->deleteGroup(*group); } int i = 0; for (const auto &identity : mIdentities) { KConfigGroup cg(mConfig, QStringLiteral("Identity #%1").arg(i)); identity.writeConfig(cg); } mConfig->sync(); } void IdentityManager::readConfig(KConfig *config) { mIdentities.clear(); const QStringList identities = groupList(config); if (identities.isEmpty()) { return; // nothing to be done... } QStringList::const_iterator groupEnd = identities.constEnd(); for (QStringList::const_iterator group = identities.constBegin(); group != groupEnd; ++group) { KConfigGroup configGroup(config, *group); KIdentityManagementCore::Identity identity; identity.readConfig(configGroup); // Don't load invalid identity if (!identity.isNull() && !identity.primaryEmailAddress().isEmpty()) { mIdentities << identity; } } std::sort(mIdentities.begin(), mIdentities.end()); } QStringList IdentityManager::groupList(KConfig *config) const { return config->groupList().filter(QRegularExpression(QStringLiteral("^Identity #\\d+$"))); }