Page MenuHome GnuPG

No OneTemporary

diff --git a/client/editor/composerwindow.cpp b/client/editor/composerwindow.cpp
index 6013891..0a6505b 100644
--- a/client/editor/composerwindow.cpp
+++ b/client/editor/composerwindow.cpp
@@ -1,1718 +1,1726 @@
// SPDX-FileCopyrightText: 2023 g10 code Gmbh
// SPDX-Contributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "composerwindow.h"
// Qt includes
#include <QApplication>
#include <QClipboard>
#include <QCloseEvent>
#include <QFileDialog>
#include <QFileInfo>
#include <QInputDialog>
#include <QLabel>
#include <QMenu>
#include <QMimeData>
#include <QPainter>
#include <QPrintDialog>
#include <QPrintPreviewDialog>
#include <QPrinter>
#include <QProcess>
#include <QPushButton>
#include <QSplitter>
#include <QStatusBar>
#include <QTimer>
#include <QUrlQuery>
#include <QVBoxLayout>
// KDE includes
#include "identity/identity.h"
#include <KActionCollection>
#include <KColorScheme>
#include <KCursorSaver>
#include <KEmailAddress>
#include <KIconUtils>
#include <KLineEdit>
#include <KLocalizedString>
#include <KMessageBox>
#include <KMime/Message>
#include <KToolBar>
#include <MimeTreeParserWidgets/MessageViewer>
#include <MimeTreeParserWidgets/MessageViewerDialog>
#include <Sonnet/DictionaryComboBox>
#include <Libkleo/Compliance>
#include <Libkleo/ExpiryChecker>
#include <Libkleo/ExpiryCheckerSettings>
#include <Libkleo/KeyCache>
#include <Libkleo/KeyResolverCore>
#include <Libkleo/KeySelectionCombo>
#include <Libkleo/KeySelectionDialog>
// Gpgme includes
#include <QGpgME/Protocol>
#include <gpgme++/tofuinfo.h>
// App includes
#include "../identity/identitydialog.h"
#include "../identity/identitymanager.h"
#include "../mailapi.h"
#include "attachment/attachmentcontroller.h"
#include "attachment/attachmentmodel.h"
#include "attachment/attachmentview.h"
#include "bodytexteditor.h"
#include "composerviewbase.h"
#include "draft/draftmanager.h"
#include "messagedispatcher.h"
#include "job/inserttextfilejob.h"
#include "job/saveasfilejob.h"
#include "kmcomposerglobalaction.h"
#include "mailtemplates.h"
#include "messagecomposersettings.h"
#include "nearexpirywarning.h"
#include "recipientseditor.h"
#include "signaturecontroller.h"
#include "spellcheckerconfigdialog.h"
#include "websocketclient.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<GpgME::UserID> &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, 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))
, mBodyTextEditor(new MessageComposer::BodyTextEditor(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);
slotIdentityChanged();
});
} else if (mIdentity.fullName().isEmpty()) {
mIdentity.setFullName(name);
IdentityManager::self().updateIdentity(mIdentity);
}
mMessageDispatcher = MailApiController::self().messageDispatcher(this);
mComposerBase->setMessageDispatcher(mMessageDispatcher);
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<int> 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);
mComposerBase->setFrom(mIdentity.fullName() + u" <" + mFrom + u'>');
mEdtFrom->setObjectName(QStringLiteral("fromLine"));
fromWrapperLayout->addWidget(mEdtFrom);
mComposerBase->setIdentity(mIdentity);
mButtonFrom->setText(i18nc("@action:button", "Configure"));
mButtonFrom->setIcon(QIcon::fromTheme(u"configure-symbolic"_s));
mButtonFrom->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
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<RecipientLineNG *>(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++;
connect(mEdtSubject, &QLineEdit::textEdited, this, [this]() {
mComposerBase->setSubject(mEdtSubject->text());
});
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::stateChanged, this, [&websocketClient, connectionLossWidget]() {
if (websocketClient.state() == WebsocketClient::State::ConnectedToWebclient) {
connectionLossWidget->hide();
} else {
connectionLossWidget->setText(websocketClient.stateDisplay());
connectionLossWidget->show();
}
});
// Rich text editor
mBodyTextEditor->setProperty("_breeze_borders_sides", QVariant::fromValue(QFlags{Qt::TopEdge}));
mBodyTextEditor->setProperty("_breeze_force_frame", true);
mComposerBase->setEditor(mBodyTextEditor);
vLayout->addWidget(mBodyTextEditor);
auto attachmentModel = new MessageComposer::AttachmentModel(this);
auto attachmentView = new AttachmentView(attachmentModel, mHeadersToEditorSplitter);
attachmentView->hideIfEmpty();
connect(attachmentView, &AttachmentView::modified, this, &ComposerWindow::setModified);
connect(attachmentView, &AttachmentView::errorOccurred, this, &ComposerWindow::slotHandleError);
auto attachmentController = new AttachmentController(attachmentModel, attachmentView, this);
mComposerBase->setAttachmentController(attachmentController);
mComposerBase->setAttachmentModel(attachmentModel);
auto signatureController = new MessageComposer::SignatureController(this);
signatureController->setIdentity(mIdentity);
signatureController->setEditor(mComposerBase->editor());
mComposerBase->setSignatureController(signatureController);
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("<p>"), QStringLiteral(" "))
.replace(QStringLiteral("</p>"), QStringLiteral(" "))
.replace(QStringLiteral("<p align=center>"), QStringLiteral(" "));
mNearExpiryWarning->addInfo(plainMsg);
mNearExpiryWarning->setWarning(info == Kleo::ExpiryChecker::OwnKeyExpired);
mNearExpiryWarning->animatedShow();
}
const QList<KPIM::MultiplyingLine *> lstLines = mRecipientEditor->lines();
for (KPIM::MultiplyingLine *line : lstLines) {
auto recipient = line->data().dynamicCast<Recipient>();
if (recipient->key().primaryFingerprint() == key.primaryFingerprint()) {
auto recipientLine = qobject_cast<RecipientLineNG *>(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();
runKeyResolver();
connect(mMessageDispatcher, &MessageDispatcher::errorOccurred, this, [this](const QString &mailId, const QString &errorMessage) {
if (mailId == mComposerBase->mailId()) {
KMessageBox::error(this, errorMessage);
}
});
connect(mMessageDispatcher, &MessageDispatcher::sentSuccessfully, this, [this](const QString &mailId) {
if (mailId == mComposerBase->mailId()) {
auto &draftManager = DraftManager::self();
draftManager.remove(draftManager.draftById(mailId.toUtf8()));
slotCloseWindow();
}
});
connect(mComposerBase, &MessageComposer::ComposerViewBase::failed, this, &ComposerWindow::slotHandleError);
connect(mComposerBase, &MessageComposer::ComposerViewBase::closeWindow, this, &ComposerWindow::slotCloseWindow);
}
void ComposerWindow::slotHandleError(const QString &errorMessage)
{
KMessageBox::error(this, errorMessage);
}
void ComposerWindow::reset(const QString &fromAddress, const QString &name)
{
mMessageDispatcher->deleteLater();
mMessageDispatcher = MailApiController::self().messageDispatcher(this);
mComposerBase->setMessageDispatcher(mMessageDispatcher);
mFrom = fromAddress;
mEdtFrom->setText(mFrom);
mComposerBase->setFrom(mIdentity.fullName() + u" <" + mFrom + u'>');
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);
});
}
mEdtSubject->setText(QString());
mRecipientEditor->clear();
mComposerBase->editor()->setText(QString{});
mComposerBase->attachmentController()->clear();
const QList<KPIM::MultiplyingLine *> lstLines = mRecipientEditor->lines();
for (KPIM::MultiplyingLine *line : lstLines) {
slotRecipientEditorLineAdded(qobject_cast<RecipientLineNG *>(line));
}
}
void ComposerWindow::setupActions()
{
// Save as draft
auto action = new QAction(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save as Encrypted &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(), &MessageComposer::BodyTextEditor::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
mWordWrapAction = new KToggleAction(i18n("&Wordwrap"), this);
actionCollection()->addAction(QStringLiteral("wordwrap"), mWordWrapAction);
mWordWrapAction->setChecked(MessageComposer::MessageComposerSettings::self()->wordWrap());
connect(mWordWrapAction, &KToggleAction::toggled, this, &ComposerWindow::slotWordWrapToggled);
slotWordWrapToggled(MessageComposer::MessageComposerSettings::self()->wordWrap());
// 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);
// Auto-attach keys // TODO: sort out UI
auto autoAttachSelect = new KSelectAction(i18n("Auto-Attach OpenPGP keys"), this);
actionCollection()->addAction(QStringLiteral("auto_attach_keys"), autoAttachSelect);
autoAttachSelect->setToolTip(i18n("When sending the E-Mail, you may want to attach your own public key, and - in case of multiple recipients (in CC: or To:) - the public keys of the further participants. This ensures the receiver(s) have the required key(s) to encrypt their reply."));
auto noAutoKeys = autoAttachSelect->addAction(i18n("Do not auto-attach keys"));
noAutoKeys->setChecked(true); // TODO: read/write config
mAutoAttachOwnKeyAction = autoAttachSelect->addAction(i18n("Attach my own public key"));
mAutoAttachRecipientKeysAction = autoAttachSelect->addAction(i18n("Attach all relevant public keys"));
// 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);
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);
const QList<KPIM::MultiplyingLine *> lstLines = mRecipientEditor->lines();
for (KPIM::MultiplyingLine *line : lstLines) {
slotRecipientEditorLineAdded(qobject_cast<RecipientLineNG *>(line));
}
}
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);
if (Kleo::DeVSCompliance::isActive()) {
auto statusLbl = std::make_unique<QLabel>(Kleo::DeVSCompliance::name());
{
auto statusPalette = qApp->palette();
KColorScheme::adjustForeground(statusPalette,
Kleo::DeVSCompliance::isCompliant() ? KColorScheme::NormalText : KColorScheme::NegativeText,
statusLbl->foregroundRole(),
KColorScheme::View);
statusLbl->setAutoFillBackground(true);
KColorScheme::adjustBackground(statusPalette,
Kleo::DeVSCompliance::isCompliant() ? KColorScheme::PositiveBackground : KColorScheme::NegativeBackground,
QPalette::Window,
KColorScheme::View);
statusLbl->setPalette(statusPalette);
}
statusBar()->addPermanentWidget(statusLbl.release());
}
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();
});
}
QString ComposerWindow::mailId() const
{
if (!mComposerBase) {
return {};
}
return mComposerBase->mailId();
}
void ComposerWindow::setMailId(const QString &mailId)
{
if (!mComposerBase) {
return;
}
mComposerBase->setMailId(mailId);
Q_EMIT initialized();
}
void ComposerWindow::setMessage(const KMime::Message::Ptr &msg)
{
mComposerBase->setMessage(msg);
mEdtSubject->setText(mComposerBase->subject());
}
bool ComposerWindow::isComposerModified() const
{
return mComposerBase->editor()->document()->isModified() || mComposerBase->recipientsEditor()->isModified() || mEdtSubject->isModified();
}
void ComposerWindow::setModified(bool isModified)
{
mIsModified = isModified;
mComposerBase->editor()->document()->setModified(isModified);
if (!isModified) {
mComposerBase->recipientsEditor()->clearModified();
mEdtSubject->setModified(false);
}
}
bool ComposerWindow::isModified() const
{
return mIsModified || isComposerModified();
}
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("<qt><p>In order to be able to sign "
"this message you first have to "
"define the (OpenPGP or S/MIME) signing key "
"to use.</p>"
"<p>Please select the key to use "
"in the identity configuration.</p>"
"</qt>"),
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<Kleo::KeyResolverCore> ComposerWindow::fillKeyResolver()
{
auto keyResolverCore = std::make_unique<Kleo::KeyResolverCore>(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<Recipient>();
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 {
const auto lst = mRecipientEditor->lines();
bool empty = false;
if (lst.size() == 1) {
const auto line = qobject_cast<RecipientLineNG *>(lst.first());
if (line->recipientsCount() == 0) {
empty = true;
}
}
if (state && !empty) {
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<RecipientLineNG *>(lst.first());
if (line->recipientsCount() == 0) {
mAcceptedSolution = false;
slotEncryptionButtonIconUpdate();
return;
}
}
mAcceptedSolution = result.flags & Kleo::KeyResolverCore::AllResolved;
for (auto line_ : lst) {
auto line = qobject_cast<RecipientLineNG *>(line_);
Q_ASSERT(line);
auto recipient = line->data().dynamicCast<Recipient>();
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);
}
}
slotEncryptionButtonIconUpdate();
}
void ComposerWindow::annotateRecipientEditorLineWithCryptoInfo(RecipientLineNG *line)
{
auto recipient = line->data().dynamicCast<Recipient>();
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 = NoTrusted;
} 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();
}
// Ensure the tooltips are word wrapped
tooltip = u"<div>"_s + tooltip + u"</div>"_s;
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) {
if (keyState == NoTrusted) {
line->setIcon(QIcon::fromTheme(QStringLiteral("emblem-question")), tooltip);
} else {
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()
{
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();
if (mAutoAttachOwnKeyAction->isChecked() || mAutoAttachRecipientKeysAction->isChecked()) {
QStringList attachedKeyIds;
const auto &ownkey = mComposerBase->identity().pgpEncryptionKey();
auto key = Kleo::KeyCache::instance()->findByFingerprint(ownkey.data());
mComposerBase->attachmentController()->attachPublicKey(key, AttachmentController::Synchronous);
attachedKeyIds.append(QString::fromLatin1(key.keyID()));
if (mAutoAttachRecipientKeysAction->isChecked()) {
- const auto recipients = mRecipientEditor->recipients();
- for (const auto &recipient : recipients) {
- const auto key = recipient->key();
- if (key.isNull() || attachedKeyIds.contains(QString::fromLatin1(key.keyID()))) {
- continue;
+ // First, filter out BCC
+ auto recipients = mRecipientEditor->recipients();
+ auto remove = std::remove_if(recipients.begin(), recipients.end(), [](const auto &recipient) {
+ return (recipient->type() == Recipient::Bcc);
+ });
+ recipients.erase(remove, recipients.end());
+ // Attach recipient keys, only if more than one recipient
+ if (recipients.length() > 1) {
+ for (const auto &recipient : std::as_const(recipients)) {
+ const auto key = recipient->key();
+ if (key.isNull() || attachedKeyIds.contains(QString::fromLatin1(key.keyID()))) {
+ continue;
+ }
+ mComposerBase->attachmentController()->attachPublicKey(key, AttachmentController::Synchronous);
}
- mComposerBase->attachmentController()->attachPublicKey(key, AttachmentController::Synchronous);
}
}
}
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);
mComposerBase->setCryptoOptions(true, true, cryptoMessageFormat());
}
}
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<Kleo::ExpiryChecker> 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<KMail::IdentityDialog> 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->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<KIdentityManagementCore::Identity &>(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()->toPlainText();
}
RecipientsEditor *ComposerWindow::recipientsEditor() const
{
return mRecipientEditor;
}
void ComposerWindow::addAttachment(const QList<AttachmentInfo> &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 QByteArray &data, const QByteArray &mimeType)
{
Q_UNUSED(cte)
mComposerBase->addAttachment(name, name, data, mimeType);
}
void ComposerWindow::insertUrls(const QMimeData *source, const QList<QUrl> &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()->textCursor().insertText(urlStr + QLatin1Char('\n'));
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() && 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;
}
// 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, imageData, "image/png");
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<QUrl> 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<AttachmentInfo> 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<AttachmentInfo> 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->slotSaveDraft();
}
void ComposerWindow::slotSaveAsFile()
{
auto job = new SaveAsFileJob(this);
job->setParentWidget(this);
job->setHtmlMode(false);
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
{
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->generateMessage([printer](const QList<KMime::Message::Ptr> &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, 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<SpellCheckerConfigDialog> 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()
{
if (isModified()) {
const QString savebut = i18n("&Save as Draft");
const QString savetext = i18n("Save this message encrypted in your drafts folder. It can then be edited and sent at a later time.");
const int rc = KMessageBox::warningTwoActionsCancel(this,
i18n("Do you want to save the message for later or discard it?"),
i18nc("@title:window", "Close Composer"),
KGuiItem(savebut, QStringLiteral("document-save"), QString(), savetext),
KStandardGuiItem::discard(),
KStandardGuiItem::cancel());
if (rc == KMessageBox::Cancel) {
return false;
} else if (rc == KMessageBox::ButtonCode::PrimaryAction) {
// doSend will close the window. Just return false from this method
slotSaveDraft();
return false;
}
// else fall through: return true
}
mComposerBase->cleanupAutoSave();
return true;
}
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<Recipient>();
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",
"<qt>No valid and trusted encryption certificate was "
"found for \"%1\".<br/><br/>"
"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.</qt>",
recipient->name().isEmpty() ? recipient->email() : recipient->name());
const bool opgp = containsOpenPGP(cryptoMessageFormat());
const bool x509 = containsSMIME(cryptoMessageFormat());
QPointer<Kleo::KeySelectionDialog> 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();
}
}
void ComposerWindow::slotCloseWindow()
{
setModified(false);
mComposerBase->cleanupAutoSave();
hide();
}

File Metadata

Mime Type
text/x-diff
Expires
Thu, Feb 26, 7:05 PM (16 h, 34 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
06/0b/5b2155865eb23bd261cd083d70a1

Event Timeline