diff --git a/server/editor/composerviewbase.cpp b/server/editor/composerviewbase.cpp index 24893ec..86c8ab4 100644 --- a/server/editor/composerviewbase.cpp +++ b/server/editor/composerviewbase.cpp @@ -1,1361 +1,1352 @@ /* SPDX-FileCopyrightText: 2010 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.com SPDX-FileCopyrightText: 2010 Leo Franchi SPDX-License-Identifier: LGPL-2.0-or-later */ #include "composerviewbase.h" #include "attachment/attachmentcontrollerbase.h" #include "attachment/attachmentmodel.h" #include "richtextcomposerng.h" #include "richtextcomposersignatures.h" #include "composer.h" #include "nodehelper.h" #include "signaturecontroller.h" #include "part/globalpart.h" #include "part/infopart.h" #include "util.h" #include "util_p.h" #include "ews/ewsmailfactory.h" #include "mailtemplates.h" #include "../qnam.h" #include "messagecomposersettings.h" #include "recipientseditor.h" #include #include #include #include #include #include #include #include "editor_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "draft/draftmanager.h" using namespace MessageComposer; using namespace Qt::Literals::StringLiterals; ComposerViewBase::ComposerViewBase(QObject *parent, QWidget *parentGui) : QObject(parent) , m_msg(KMime::Message::Ptr(new KMime::Message)) , m_parentWidget(parentGui) , m_cryptoMessageFormat(Kleo::AutoFormat) , m_autoSaveInterval(60000) // default of 1 min { m_charsets << "utf-8"; // default, so we have a backup in case client code forgot to set. initAutoSave(); connect(this, &ComposerViewBase::composerCreated, this, &ComposerViewBase::slotComposerCreated); } ComposerViewBase::~ComposerViewBase() = default; bool ComposerViewBase::isComposing() const { return !m_composers.isEmpty(); } -void ComposerViewBase::setMessage(const KMime::Message::Ptr &msg, bool allowDecryption) +void ComposerViewBase::setMessage(const KMime::Message::Ptr &msg) { if (m_attachmentModel) { const auto attachments{m_attachmentModel->attachments()}; for (const MessageCore::AttachmentPart::Ptr &attachment : attachments) { if (!m_attachmentModel->removeAttachment(attachment)) { qCWarning(EDITOR_LOG) << "Attachment not found."; } } } m_msg = msg; if (m_recipientsEditor) { m_recipientsEditor->clear(); bool resultTooManyRecipients = m_recipientsEditor->setRecipientString(m_msg->to()->mailboxes(), Recipient::To); if (!resultTooManyRecipients) { resultTooManyRecipients = m_recipientsEditor->setRecipientString(m_msg->cc()->mailboxes(), Recipient::Cc); } if (!resultTooManyRecipients) { resultTooManyRecipients = m_recipientsEditor->setRecipientString(m_msg->bcc()->mailboxes(), Recipient::Bcc); } if (!resultTooManyRecipients) { resultTooManyRecipients = m_recipientsEditor->setRecipientString(m_msg->replyTo()->mailboxes(), Recipient::ReplyTo); } m_recipientsEditor->setFocusBottom(); Q_EMIT tooManyRecipient(resultTooManyRecipients); } // First, we copy the message and then parse it to the object tree parser. // The otp gets the message text out of it, in textualContent(), and also decrypts // the message if necessary. auto msgContent = new KMime::Content; msgContent->setContent(m_msg->encodedContent()); msgContent->parse(); - //MimeTreeParser::ObjectTreeParser otp(); // All default are ok - //emptySource.setDecryptMessage(allowDecryption); - //otp.parseObjectTree(msgContent); // Load the attachments - //const auto attachmentsOfExtraContents{otp.nodeHelper()->attachmentsOfExtraContents()}; - //for (const auto &att : attachmentsOfExtraContents) { - // addAttachmentPart(att); - //} const auto attachments{msgContent->attachments()}; for (const auto &att : attachments) { addAttachmentPart(att); } // Set the HTML text and collect HTML images bool isHtml = false; const auto body = MailTemplates::body(msg, isHtml); if (isHtml) { enableHtml(); } else { disableHtml(LetUserConfirm); } editor()->setText(body); if (auto hdr = m_msg->headerByType("X-KMail-CursorPos")) { m_editor->setCursorPositionFromStart(hdr->asUnicodeString().toUInt()); } delete msgContent; } void ComposerViewBase::saveMailSettings() { auto header = new KMime::Headers::Generic("X-KMail-Identity"); header->fromUnicodeString(QString::number(m_identity.uoid()), "utf-8"); m_msg->setHeader(header); header = new KMime::Headers::Generic("X-KMail-Identity-Name"); header->fromUnicodeString(m_identity.identityName(), "utf-8"); m_msg->setHeader(header); header = new KMime::Headers::Generic("X-KMail-Dictionary"); header->fromUnicodeString(m_dictionary->currentDictionary(), "utf-8"); m_msg->setHeader(header); // Save the quote prefix which is used for this message. Each message can have // a different quote prefix, for example depending on the original sender. if (m_editor->quotePrefixName().isEmpty()) { m_msg->removeHeader("X-KMail-QuotePrefix"); } else { header = new KMime::Headers::Generic("X-KMail-QuotePrefix"); header->fromUnicodeString(m_editor->quotePrefixName(), "utf-8"); m_msg->setHeader(header); } if (m_editor->composerControler()->isFormattingUsed()) { qCDebug(EDITOR_LOG) << "HTML mode"; header = new KMime::Headers::Generic("X-KMail-Markup"); header->fromUnicodeString(QStringLiteral("true"), "utf-8"); m_msg->setHeader(header); } else { m_msg->removeHeader("X-KMail-Markup"); qCDebug(EDITOR_LOG) << "Plain text"; } } void ComposerViewBase::clearFollowUp() { mFollowUpDate = QDate(); } void ComposerViewBase::send() { KCursorSaver saver(Qt::WaitCursor); saveMailSettings(); if (m_editor->composerControler()->isFormattingUsed() && inlineSigningEncryptionSelected()) { const QString keepBtnText = m_encrypt ? m_sign ? i18n("&Keep markup, do not sign/encrypt") : i18n("&Keep markup, do not encrypt") : i18n("&Keep markup, do not sign"); const QString yesBtnText = m_encrypt ? m_sign ? i18n("Sign/Encrypt (delete markup)") : i18n("Encrypt (delete markup)") : i18n("Sign (delete markup)"); int ret = KMessageBox::warningTwoActionsCancel(m_parentWidget, i18n("

Inline signing/encrypting of HTML messages is not possible;

" "

do you want to delete your markup?

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

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

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

"), QStringLiteral(" ")); mNearExpiryWarning->addInfo(plainMsg); mNearExpiryWarning->setWarning(info == Kleo::ExpiryChecker::OwnKeyExpired); mNearExpiryWarning->animatedShow(); } const QList lstLines = mRecipientEditor->lines(); for (KPIM::MultiplyingLine *line : lstLines) { auto recipient = line->data().dynamicCast(); if (recipient->key().primaryFingerprint() == key.primaryFingerprint()) { auto recipientLine = qobject_cast(line); QString iconname = QStringLiteral("emblem-warning"); if (info == Kleo::ExpiryChecker::OtherKeyExpired) { mEncryptionState.setAcceptedSolution(false); iconname = QStringLiteral("emblem-error"); const auto showCryptoIndicator = true; const auto hasOverride = mEncryptionState.hasOverride(); const auto encrypt = mEncryptionState.encrypt(); const bool showAllIcons = showCryptoIndicator && hasOverride && encrypt; if (!showAllIcons) { recipientLine->setIcon(QIcon(), msg); return; } } recipientLine->setIcon(QIcon::fromTheme(iconname), msg); return; } } }); // TODO make it possible to show this auto dictionaryComboBox = new Sonnet::DictionaryComboBox(this); dictionaryComboBox->hide(); mComposerBase->setDictionary(dictionaryComboBox); slotIdentityChanged(); } void ComposerWindow::reset(const QString &fromAddress, const QString &name, const QByteArray &bearerToken) { mFrom = fromAddress; bool isNew = false; mIdentity = IdentityManager::self().fromEmail(fromAddress, isNew); if (isNew) { // fill the idenity with default fields auto dlg = new KMail::IdentityDialog; mIdentity.setFullName(name); dlg->setIdentity(mIdentity); connect(dlg, &KMail::IdentityDialog::keyListingFinished, this, [this, dlg]() { dlg->updateIdentity(mIdentity); IdentityManager::self().updateIdentity(mIdentity); }); } mComposerBase->setBearerToken(bearerToken); mEdtSubject->setText(QString()); mRecipientEditor->clear(); mComposerBase->editor()->setText(QString{}); mComposerBase->attachmentController()->clear(); } void ComposerWindow::setupActions() { // Save as draft auto action = new QAction(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save as &Draft"), this); actionCollection()->addAction(QStringLiteral("save_in_drafts"), action); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL | Qt::Key_S)); connect(action, &QAction::triggered, this, &ComposerWindow::slotSaveDraft); // Save as file action = new QAction(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save as &File"), this); actionCollection()->addAction(QStringLiteral("save_as_file"), action); connect(action, &QAction::triggered, this, &ComposerWindow::slotSaveAsFile); // Insert file action = new QAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("&Insert Text File..."), this); actionCollection()->addAction(QStringLiteral("insert_file"), action); connect(action, &QAction::triggered, this, &ComposerWindow::slotInsertFile); // Spellchecking mAutoSpellCheckingAction = new KToggleAction(QIcon::fromTheme(QStringLiteral("tools-check-spelling")), i18n("&Automatic Spellchecking"), this); actionCollection()->addAction(QStringLiteral("options_auto_spellchecking"), mAutoSpellCheckingAction); const bool spellChecking = MessageComposer::MessageComposerSettings::self()->autoSpellChecking(); mAutoSpellCheckingAction->setChecked(spellChecking); slotAutoSpellCheckingToggled(spellChecking); connect(mAutoSpellCheckingAction, &KToggleAction::toggled, this, &ComposerWindow::slotAutoSpellCheckingToggled); connect(mComposerBase->editor(), &TextCustomEditor::RichTextEditor::checkSpellingChanged, this, &ComposerWindow::slotAutoSpellCheckingToggled); action = new QAction(QIcon::fromTheme(QStringLiteral("tools-check-spelling")), i18n("&Spellchecker..."), this); action->setIconText(i18n("Spellchecker")); actionCollection()->addAction(QStringLiteral("setup_spellchecker"), action); connect(action, &QAction::triggered, this, &ComposerWindow::slotSpellcheckConfig); // Recent actions mRecentAction = new KRecentFilesAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("&Insert Recent Text File"), this); actionCollection()->addAction(QStringLiteral("insert_file_recent"), mRecentAction); connect(mRecentAction, &KRecentFilesAction::urlSelected, this, &ComposerWindow::slotInsertRecentFile); connect(mRecentAction, &KRecentFilesAction::recentListCleared, this, &ComposerWindow::slotRecentListFileClear); const QStringList urls = MessageComposer::MessageComposerSettings::self()->recentUrls(); for (const QString &url : urls) { mRecentAction->addUrl(QUrl(url)); } // print KStandardAction::print(this, &ComposerWindow::slotPrint, actionCollection()); KStandardAction::printPreview(this, &ComposerWindow::slotPrintPreview, actionCollection()); // Send email action action = new QAction(QIcon::fromTheme(QStringLiteral("mail-send")), i18n("&Send Mail"), this); actionCollection()->addAction(QStringLiteral("mail_send"), action); actionCollection()->setDefaultShortcut(action, QKeySequence(Qt::CTRL | Qt::Key_Return)); connect(action, &QAction::triggered, this, &ComposerWindow::slotSend); // Toggle rich text mMarkupAction = new KToggleAction(i18n("Rich Text Editing"), this); mMarkupAction->setIcon(QIcon::fromTheme(QStringLiteral("preferences-desktop-font"))); mMarkupAction->setIconText(i18n("Rich Text")); mMarkupAction->setToolTip(i18n("Toggle rich text editing mode")); actionCollection()->addAction(QStringLiteral("html"), mMarkupAction); connect(mMarkupAction, &KToggleAction::triggered, this, &ComposerWindow::slotToggleMarkup); mWordWrapAction = new KToggleAction(i18n("&Wordwrap"), this); actionCollection()->addAction(QStringLiteral("wordwrap"), mWordWrapAction); mWordWrapAction->setChecked(MessageComposer::MessageComposerSettings::self()->wordWrap()); connect(mWordWrapAction, &KToggleAction::toggled, this, &ComposerWindow::slotWordWrapToggled); // Encryption action mEncryptAction = new KToggleAction(QIcon::fromTheme(QStringLiteral("document-encrypt")), i18n("&Encrypt Message"), this); mEncryptAction->setIconText(i18n("Encrypt")); actionCollection()->addAction(QStringLiteral("encrypt_message"), mEncryptAction); connect(&mEncryptionState, &EncryptionState::possibleEncryptChanged, mEncryptAction, &KToggleAction::setEnabled); connect(mEncryptAction, &KToggleAction::triggered, &mEncryptionState, &EncryptionState::toggleOverride); // Signing action mSignAction = new KToggleAction(QIcon::fromTheme(QStringLiteral("document-sign")), i18n("&Sign Message"), this); mSignAction->setIconText(i18n("Sign")); actionCollection()->addAction(QStringLiteral("sign_message"), mSignAction); connect(&mEncryptionState, &EncryptionState::possibleEncryptChanged, mEncryptAction, &KToggleAction::setEnabled); connect(&mEncryptionState, &EncryptionState::encryptChanged, mEncryptAction, &KToggleAction::setChecked); connect(mSignAction, &KToggleAction::triggered, this, &ComposerWindow::slotSignToggled); // Append signature mAppendSignature = new QAction(i18n("Append S&ignature"), this); actionCollection()->addAction(QStringLiteral("append_signature"), mAppendSignature); connect(mAppendSignature, &QAction::triggered, mComposerBase->signatureController(), &MessageComposer::SignatureController::appendSignature); // Prepend signature mPrependSignature = new QAction(i18n("Pr&epend Signature"), this); actionCollection()->addAction(QStringLiteral("prepend_signature"), mPrependSignature); connect(mPrependSignature, &QAction::triggered, mComposerBase->signatureController(), &MessageComposer::SignatureController::prependSignature); mInsertSignatureAtCursorPosition = new QAction(i18n("Insert Signature At C&ursor Position"), this); actionCollection()->addAction(QStringLiteral("insert_signature_at_cursor_position"), mInsertSignatureAtCursorPosition); connect(mInsertSignatureAtCursorPosition, &QAction::triggered, mComposerBase->signatureController(), &MessageComposer::SignatureController::insertSignatureAtCursor); action = new QAction(i18n("Paste as Attac&hment"), this); actionCollection()->addAction(QStringLiteral("paste_att"), action); connect(action, &QAction::triggered, this, &ComposerWindow::slotPasteAsAttachment); action = new QAction(i18n("Cl&ean Spaces"), this); actionCollection()->addAction(QStringLiteral("clean_spaces"), action); connect(action, &QAction::triggered, mComposerBase->signatureController(), &MessageComposer::SignatureController::cleanSpace); mRichTextComposer->composerActions()->createActions(actionCollection()); KStandardAction::close(this, &ComposerWindow::close, actionCollection()); KStandardAction::undo(mGlobalAction, &KMComposerGlobalAction::slotUndo, actionCollection()); KStandardAction::redo(mGlobalAction, &KMComposerGlobalAction::slotRedo, actionCollection()); KStandardAction::cut(mGlobalAction, &KMComposerGlobalAction::slotCut, actionCollection()); KStandardAction::copy(mGlobalAction, &KMComposerGlobalAction::slotCopy, actionCollection()); KStandardAction::paste(mGlobalAction, &KMComposerGlobalAction::slotPaste, actionCollection()); mSelectAll = KStandardAction::selectAll(mGlobalAction, &KMComposerGlobalAction::slotMarkAll, actionCollection()); mFindText = KStandardAction::find(mRichTextEditorWidget, &TextCustomEditor::RichTextEditorWidget::slotFind, actionCollection()); mFindNextText = KStandardAction::findNext(mRichTextEditorWidget, &TextCustomEditor::RichTextEditorWidget::slotFindNext, actionCollection()); mReplaceText = KStandardAction::replace(mRichTextEditorWidget, &TextCustomEditor::RichTextEditorWidget::slotReplace, actionCollection()); mComposerBase->attachmentController()->createActions(); createGUI(u"composerui.rc"_s); connect(toolBar(QStringLiteral("htmlToolBar"))->toggleViewAction(), &QAction::toggled, this, &ComposerWindow::htmlToolBarVisibilityChanged); connect(&mEncryptionState, &EncryptionState::encryptChanged, this, &ComposerWindow::slotEncryptionButtonIconUpdate); connect(&mEncryptionState, &EncryptionState::encryptChanged, this, &ComposerWindow::updateSignatureAndEncryptionStateIndicators); connect(&mEncryptionState, &EncryptionState::overrideChanged, this, &ComposerWindow::slotEncryptionButtonIconUpdate); connect(&mEncryptionState, &EncryptionState::overrideChanged, this, &ComposerWindow::runKeyResolver); connect(&mEncryptionState, &EncryptionState::acceptedSolutionChanged, this, &ComposerWindow::slotEncryptionButtonIconUpdate); } void ComposerWindow::setupStatusBar(QWidget *w) { statusBar()->addWidget(w); mStatusbarLabel = new QLabel(this); mStatusbarLabel->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); statusBar()->addPermanentWidget(mStatusbarLabel); mCursorLineLabel = new QLabel(this); mCursorLineLabel->setTextFormat(Qt::PlainText); mCursorLineLabel->setText(i18nc("Shows the linenumber of the cursor position.", " Line: %1 ", QStringLiteral(" "))); statusBar()->addPermanentWidget(mCursorLineLabel); mCursorColumnLabel = new QLabel(i18n(" Column: %1 ", QStringLiteral(" "))); mCursorColumnLabel->setTextFormat(Qt::PlainText); statusBar()->addPermanentWidget(mCursorColumnLabel); connect(mComposerBase->editor(), &QTextEdit::cursorPositionChanged, this, &ComposerWindow::slotCursorPositionChanged); slotCursorPositionChanged(); } void ComposerWindow::reply(const KMime::Message::Ptr &originalMessage) { MailTemplates::reply(originalMessage, [this](const KMime::Message::Ptr &message) { setMessage(message); Q_EMIT initialized(); }); } void ComposerWindow::forward(const KMime::Message::Ptr &originalMessage) { MailTemplates::forward(originalMessage, [this](const KMime::Message::Ptr &message) { setMessage(message); Q_EMIT initialized(); }); } void ComposerWindow::setMessage(const KMime::Message::Ptr &msg) { mEdtSubject->setText(msg->subject()->asUnicodeString()); - mComposerBase->setMessage(msg, true); + 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); if (!setByUser) { updateSignatureAndEncryptionStateIndicators(); } // mark the attachments for (no) signing //if (canSignEncryptAttachments()) { // mComposerBase->attachmentModel()->setSignSelected(sign); //} } std::unique_ptr ComposerWindow::fillKeyResolver() { auto keyResolverCore = std::make_unique(true, sign()); keyResolverCore->setMinimumValidity(GpgME::UserID::Unknown); QStringList signingKeys, encryptionKeys; if (cryptoMessageFormat() & Kleo::AnyOpenPGP) { if (!mIdentity.pgpSigningKey().isEmpty()) { signingKeys.push_back(QLatin1String(mIdentity.pgpSigningKey())); } if (!mIdentity.pgpEncryptionKey().isEmpty()) { encryptionKeys.push_back(QLatin1String(mIdentity.pgpEncryptionKey())); } } if (cryptoMessageFormat() & Kleo::AnySMIME) { if (!mIdentity.smimeSigningKey().isEmpty()) { signingKeys.push_back(QLatin1String(mIdentity.smimeSigningKey())); } if (!mIdentity.smimeEncryptionKey().isEmpty()) { encryptionKeys.push_back(QLatin1String(mIdentity.smimeEncryptionKey())); } } keyResolverCore->setSender(mIdentity.fullEmailAddr()); keyResolverCore->setSigningKeys(signingKeys); keyResolverCore->setOverrideKeys({{GpgME::UnknownProtocol, {{keyResolverCore->normalizedSender(), encryptionKeys}}}}); QStringList recipients; const auto lst = mRecipientEditor->lines(); for (auto line : lst) { auto recipient = line->data().dynamicCast(); recipients.push_back(recipient->email()); } keyResolverCore->setRecipients(recipients); return keyResolverCore; } void ComposerWindow::slotEncryptionButtonIconUpdate() { const auto state = mEncryptionState.encrypt(); const auto setByUser = mEncryptionState.override(); const auto acceptedSolution = mEncryptionState.acceptedSolution(); auto icon = QIcon::fromTheme(QStringLiteral("document-encrypt")); QString tooltip; if (state) { tooltip = i18nc("@info:tooltip", "Encrypt"); } else { tooltip = i18nc("@info:tooltip", "Not Encrypt"); icon = QIcon::fromTheme(QStringLiteral("document-decrypt")); } if (acceptedSolution) { auto overlay = QIcon::fromTheme(QStringLiteral("emblem-added")); if (state) { overlay = QIcon::fromTheme(QStringLiteral("emblem-checked")); } icon = KIconUtils::addOverlay(icon, overlay, Qt::BottomRightCorner); } else { if (state && setByUser) { auto overlay = QIcon::fromTheme(QStringLiteral("emblem-warning")); icon = KIconUtils::addOverlay(icon, overlay, Qt::BottomRightCorner); } } mEncryptAction->setIcon(icon); mEncryptAction->setToolTip(tooltip); } void ComposerWindow::runKeyResolver() { auto keyResolverCore = fillKeyResolver(); auto result = keyResolverCore->resolve(); const auto lst = mRecipientEditor->lines(); if (lst.size() == 1) { const auto line = qobject_cast(lst.first()); if (line->recipientsCount() == 0) { mEncryptionState.setAcceptedSolution(false); return; } } mEncryptionState.setAcceptedSolution(result.flags & Kleo::KeyResolverCore::AllResolved); for (auto line_ : lst) { auto line = qobject_cast(line_); Q_ASSERT(line); auto recipient = line->data().dynamicCast(); QString dummy; QString addrSpec; if (KEmailAddress::splitAddress(recipient->email(), dummy, addrSpec, dummy) != KEmailAddress::AddressOk) { addrSpec = recipient->email(); } auto resolvedKeys = result.solution.encryptionKeys[addrSpec]; GpgME::Key key; if (resolvedKeys.size() == 0) { // no key found for recipient // Search for any key, also for not accepted ons, to at least give the user more info. key = Kleo::KeyCache::instance()->findBestByMailBox(addrSpec.toUtf8().constData(), GpgME::UnknownProtocol, Kleo::KeyCache::KeyUsage::Encrypt); key.update(); // We need tofu information for key. recipient->setKey(key); } else { // A key was found for recipient key = resolvedKeys.front(); if (recipient->key().primaryFingerprint() != key.primaryFingerprint()) { key.update(); // We need tofu information for key. recipient->setKey(key); } } annotateRecipientEditorLineWithCrpytoInfo(line); if (!key.isNull()) { mExpiryChecker->checkKey(key, Kleo::ExpiryChecker::EncryptionKey); } } } void ComposerWindow::annotateRecipientEditorLineWithCrpytoInfo(RecipientLineNG *line) { auto recipient = line->data().dynamicCast(); const auto key = recipient->key(); const auto showCryptoIndicator = true; const auto hasOverride = mEncryptionState.hasOverride(); const auto encrypt = mEncryptionState.encrypt(); const bool showPositiveIcons = showCryptoIndicator && encrypt; const bool showAllIcons = showCryptoIndicator && hasOverride && encrypt; QString dummy; QString addrSpec; bool invalidEmail = false; if (KEmailAddress::splitAddress(recipient->email(), dummy, addrSpec, dummy) != KEmailAddress::AddressOk) { invalidEmail = true; addrSpec = recipient->email(); } if (key.isNull()) { recipient->setEncryptionAction(Kleo::Impossible); if (showAllIcons && !invalidEmail) { const auto icon = QIcon::fromTheme(QStringLiteral("emblem-error")); line->setIcon(icon, i18nc("@info:tooltip", "No key found for the recipient.")); } else { line->setIcon(QIcon()); } line->setProperty("keyStatus", invalidEmail ? InProgress : NoKey); return; } CryptoKeyState keyState = KeyOk; if (recipient->encryptionAction() != Kleo::DoIt) { recipient->setEncryptionAction(Kleo::DoIt); } QString tooltip; const auto uids = key.userIDs(); const auto _uid = findSendersUid(addrSpec.toStdString(), uids); GpgME::UserID uid; if (_uid == uids.cend()) { uid = key.userID(0); } else { uid = *_uid; } const auto trustLevel = Kleo::trustLevel(uid); switch (trustLevel) { case Kleo::Level0: if (uid.tofuInfo().isNull()) { tooltip = i18nc("@info:tooltip", "The encryption key is not trusted. It hasn't enough validity. " "You can sign the key, if you communicated the fingerprint by another channel. " "Click the icon for details."); keyState = NoKey; } else { switch (uid.tofuInfo().validity()) { case GpgME::TofuInfo::NoHistory: tooltip = i18nc("@info:tooltip", "The encryption key is not trusted. " "It hasn't been used anywhere to guarantee it belongs to the stated person. " "By using the key will be trusted more. " "Or you can sign the key, if you communicated the fingerprint by another channel. " "Click the icon for details."); break; case GpgME::TofuInfo::Conflict: tooltip = i18nc("@info:tooltip", "The encryption key is not trusted. It has conflicting TOFU data. " "Click the icon for details."); keyState = NoKey; break; case GpgME::TofuInfo::ValidityUnknown: tooltip = i18nc("@info:tooltip", "The encryption key is not trusted. It has unknown validity in TOFU data. " "Click the icon for details."); keyState = NoKey; break; default: tooltip = i18nc("@info:tooltip", "The encryption key is not trusted. The key is marked as bad. " "Click the icon for details."); keyState = NoKey; } } break; case Kleo::Level1: tooltip = i18nc("@info:tooltip", "The encryption key is only marginally trusted and hasn't been used enough time to guarantee it belongs to the stated person. " "By using the key will be trusted more. " "Or you can sign the key, if you communicated the fingerprint by another channel. " "Click the icon for details."); break; case Kleo::Level2: if (uid.tofuInfo().isNull()) { tooltip = i18nc("@info:tooltip", "The encryption key is only marginally trusted. " "You can sign the key, if you communicated the fingerprint by another channel. " "Click the icon for details."); } else { tooltip = i18nc("@info:tooltip", "The encryption key is only marginally trusted, but has been used enough times to be very likely controlled by the stated person. " "By using the key will be trusted more. " "Or you can sign the key, if you communicated the fingerprint by another channel. " "Click the icon for details."); } break; case Kleo::Level3: tooltip = i18nc("@info:tooltip", "The encryption key is fully trusted. You can raise the security level, by signing the key. " "Click the icon for details."); break; case Kleo::Level4: tooltip = i18nc("@info:tooltip", "The encryption key is ultimately trusted or is signed by another ultimately trusted key. " "Click the icon for details."); break; default: Q_UNREACHABLE(); } if (keyState == NoKey) { mEncryptionState.setAcceptedSolution(false); if (showAllIcons) { line->setIcon(QIcon::fromTheme(QStringLiteral("emblem-error")), tooltip); } else { line->setIcon(QIcon()); } } else if (trustLevel == Kleo::Level0 && encrypt) { line->setIcon(QIcon::fromTheme(QStringLiteral("emblem-warning")), tooltip); } else if (showPositiveIcons) { // Magically, the icon name maps precisely to each trust level // line->setIcon(QIcon::fromTheme(QStringLiteral("gpg-key-trust-level-%1").arg(trustLevel)), tooltip); line->setIcon(QIcon::fromTheme(QStringLiteral("emblem-success")), tooltip); } else { line->setIcon(QIcon()); } if (line->property("keyStatus") != keyState) { line->setProperty("keyStatus", keyState); } } void ComposerWindow::slotSignToggled(bool on) { setSigning(on, true); updateSignatureAndEncryptionStateIndicators(); } void ComposerWindow::updateSignatureAndEncryptionStateIndicators() { mCryptoStateIndicatorWidget->updateSignatureAndEncrypionStateIndicators(sign(), mEncryptionState.encrypt()); } bool ComposerWindow::sign() const { return mSignAction->isChecked(); } void ComposerWindow::slotSend() { mComposerBase->setFrom(mFrom); mComposerBase->setSubject(mEdtSubject->text()); if (mComposerBase->to().isEmpty()) { if (mComposerBase->cc().isEmpty() && mComposerBase->bcc().isEmpty()) { KMessageBox::information(this, i18n("You must specify at least one receiver, " "either in the To: field or as CC or as BCC.")); return; } else { const int rc = KMessageBox::questionTwoActions(this, i18n("To: field is empty. " "Send message anyway?"), i18nc("@title:window", "No To: specified"), KGuiItem(i18n("S&end as Is"), QLatin1String("mail-send")), KGuiItem(i18n("&Specify the To field"), QLatin1String("edit-rename"))); if (rc == KMessageBox::ButtonCode::SecondaryAction) { return; } } } if (mComposerBase->subject().isEmpty()) { mEdtSubject->setFocus(); const int rc = KMessageBox::questionTwoActions(this, i18n("You did not specify a subject. " "Send message anyway?"), i18nc("@title:window", "No Subject Specified"), KGuiItem(i18n("S&end as Is"), QStringLiteral("mail-send")), KGuiItem(i18n("&Specify the Subject"), QStringLiteral("edit-rename"))); if (rc == KMessageBox::ButtonCode::SecondaryAction) { return; } } KCursorSaver saver(Qt::WaitCursor); const bool encrypt = mEncryptionState.encrypt(); mComposerBase->setCryptoOptions( sign(), encrypt, cryptoMessageFormat()); mComposerBase->send(); } void ComposerWindow::changeCryptoAction() { if (!QGpgME::openpgp() && !QGpgME::smime()) { // no crypto whatsoever mEncryptAction->setEnabled(false); mEncryptionState.setPossibleEncrypt(false); mSignAction->setEnabled(false); setSigning(false); } else { // TODO: carl const bool canOpenPGPSign = QGpgME::openpgp();// && !ident.pgpSigningKey().isEmpty(); const bool canSMIMESign = QGpgME::smime(); // && !ident.smimeSigningKey().isEmpty(); setSigning((canOpenPGPSign || canSMIMESign)); // && ident.pgpAutoSign()); } } void ComposerWindow::slotToggleMarkup() { htmlToolBarVisibilityChanged(mMarkupAction->isChecked()); } void ComposerWindow::htmlToolBarVisibilityChanged(bool visible) { if (visible) { enableHtml(); } else { disableHtml(LetUserConfirm); } } void ComposerWindow::enableHtml() { if (mForceDisableHtml) { disableHtml(NoConfirmationNeeded); return; } mRichTextComposer->activateRichText(); if (!toolBar(QStringLiteral("htmlToolBar"))->isVisible()) { // Use singleshot, as we we might actually be called from a slot that wanted to disable the // toolbar (but the messagebox in disableHtml() prevented that and called us). // The toolbar can't correctly deal with being enabled right in a slot called from the "disabled" // signal, so wait one event loop run for that. QTimer::singleShot(0, toolBar(QStringLiteral("htmlToolBar")), &QWidget::show); } if (!mMarkupAction->isChecked()) { mMarkupAction->setChecked(true); } mRichTextComposer->composerActions()->updateActionStates(); mRichTextComposer->composerActions()->setActionsEnabled(true); } void ComposerWindow::disableHtml(Confirmation confirmation) { bool forcePlainTextMarkup = false; if (confirmation == LetUserConfirm && mRichTextComposer->composerControler()->isFormattingUsed()) { int choice = KMessageBox::warningTwoActionsCancel(this, i18n("Turning HTML mode off " "will cause the text to lose the formatting. Are you sure?"), i18n("Lose the formatting?"), KGuiItem(i18n("Lose Formatting")), KGuiItem(i18n("Add Markup Plain Text")), KStandardGuiItem::cancel(), QStringLiteral("LoseFormattingWarning")); switch (choice) { case KMessageBox::Cancel: enableHtml(); return; case KMessageBox::ButtonCode::SecondaryAction: forcePlainTextMarkup = true; break; case KMessageBox::ButtonCode::PrimaryAction: break; } } mRichTextComposer->forcePlainTextMarkup(forcePlainTextMarkup); mRichTextComposer->switchToPlainText(); mRichTextComposer->composerActions()->setActionsEnabled(false); if (toolBar(QStringLiteral("htmlToolBar"))->isVisible()) { // See the comment in enableHtml() why we use a singleshot timer, similar situation here. QTimer::singleShot(0, toolBar(QStringLiteral("htmlToolBar")), &QWidget::hide); } if (mMarkupAction->isChecked()) { mMarkupAction->setChecked(false); } } inline Kleo::chrono::days encryptOwnKeyNearExpiryWarningThresholdInDays() { const int num = 30; return Kleo::chrono::days{qMax(1, num)}; } inline Kleo::chrono::days encryptKeyNearExpiryWarningThresholdInDays() { const int num = 14; return Kleo::chrono::days{qMax(1, num)}; } inline Kleo::chrono::days encryptRootCertNearExpiryWarningThresholdInDays() { const int num = 14; return Kleo::chrono::days{qMax(1, num)}; } inline Kleo::chrono::days encryptChainCertNearExpiryWarningThresholdInDays() { const int num = 14; return Kleo::chrono::days{qMax(1, num)}; } std::shared_ptr ComposerWindow::expiryChecker() { if (!mExpiryChecker) { mExpiryChecker.reset(new Kleo::ExpiryChecker{Kleo::ExpiryCheckerSettings{encryptOwnKeyNearExpiryWarningThresholdInDays(), encryptKeyNearExpiryWarningThresholdInDays(), encryptRootCertNearExpiryWarningThresholdInDays(), encryptChainCertNearExpiryWarningThresholdInDays()}}); } return mExpiryChecker; } Kleo::CryptoMessageFormat ComposerWindow::cryptoMessageFormat() const { return Kleo::AutoFormat; } void ComposerWindow::slotEditIdentity() { QPointer dlg = new KMail::IdentityDialog(); dlg->setAttribute(Qt::WA_DeleteOnClose); dlg->setIdentity(mIdentity); dlg->open(); connect(dlg, &KMail::IdentityDialog::accepted, this, [dlg, this]() { dlg->updateIdentity(mIdentity); IdentityManager::self().updateIdentity(mIdentity); IdentityManager::self().writeConfig(); slotIdentityChanged(); }); } void ComposerWindow::slotIdentityChanged() { 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 = mEncryptionState.encrypt(); } mSignAction->setEnabled(bNewIdentityHasSigningKey); if (!bNewIdentityHasSigningKey && mLastIdentityHasSigningKey) { mLastSignActionState = sign(); setSigning(false); } // restore the last state of the sign and encrypt button if (bNewIdentityHasSigningKey && !mLastIdentityHasSigningKey) { setSigning(mLastSignActionState); } mLastIdentityHasSigningKey = bNewIdentityHasSigningKey; mLastIdentityHasEncryptionKey = bNewIdentityHasEncryptionKey; const KIdentityManagementCore::Signature sig = const_cast(mIdentity).signature(); bool isEnabledSignature = sig.isEnabledSignature(); mAppendSignature->setEnabled(isEnabledSignature); mPrependSignature->setEnabled(isEnabledSignature); mInsertSignatureAtCursorPosition->setEnabled(isEnabledSignature); mEncryptionState.setPossibleEncrypt(true); changeCryptoAction(); mEncryptionState.unsetOverride(); mEncryptionState.setPossibleEncrypt(mEncryptionState.possibleEncrypt() && bNewIdentityHasEncryptionKey); mEncryptionState.setAutoEncrypt(mIdentity.pgpAutoEncrypt()); // make sure the From and BCC fields are shown if necessary if (mIdentity.pgpAutoEncrypt() && mKeyCache->initialized()) { runKeyResolver(); } Q_EMIT identityChanged(); } void ComposerWindow::checkOwnKeyExpiry(const KIdentityManagementCore::Identity &ident) { mNearExpiryWarning->clearInfo(); mNearExpiryWarning->hide(); if (cryptoMessageFormat() & Kleo::AnyOpenPGP) { if (!ident.pgpEncryptionKey().isEmpty()) { auto const key = mKeyCache->findByKeyIDOrFingerprint(ident.pgpEncryptionKey().constData()); if (key.isNull() || !key.canEncrypt()) { mNearExpiryWarning->addInfo(i18nc("The argument is as PGP fingerprint", "Your selected PGP key (%1) doesn't exist in your keyring or is not suitable for encryption.", QString::fromUtf8(ident.pgpEncryptionKey()))); mNearExpiryWarning->setWarning(true); mNearExpiryWarning->show(); } else { mComposerBase->expiryChecker()->checkKey(key, Kleo::ExpiryChecker::OwnEncryptionKey); } } if (!ident.pgpSigningKey().isEmpty()) { if (ident.pgpSigningKey() != ident.pgpEncryptionKey()) { auto const key = mKeyCache->findByKeyIDOrFingerprint(ident.pgpSigningKey().constData()); if (key.isNull() || !key.canSign()) { mNearExpiryWarning->addInfo(i18nc("The argument is as PGP fingerprint", "Your selected PGP signing key (%1) doesn't exist in your keyring or is not suitable for signing.", QString::fromUtf8(ident.pgpSigningKey()))); mNearExpiryWarning->setWarning(true); mNearExpiryWarning->show(); } else { mComposerBase->expiryChecker()->checkKey(key, Kleo::ExpiryChecker::OwnSigningKey); } } } } if (cryptoMessageFormat() & Kleo::AnySMIME) { if (!ident.smimeEncryptionKey().isEmpty()) { auto const key = mKeyCache->findByKeyIDOrFingerprint(ident.smimeEncryptionKey().constData()); if (key.isNull() || !key.canEncrypt()) { mNearExpiryWarning->addInfo(i18nc("The argument is as SMIME fingerprint", "Your selected SMIME key (%1) doesn't exist in your keyring or is not suitable for encryption.", QString::fromUtf8(ident.smimeEncryptionKey()))); mNearExpiryWarning->setWarning(true); mNearExpiryWarning->show(); } else { mComposerBase->expiryChecker()->checkKey(key, Kleo::ExpiryChecker::OwnEncryptionKey); } } if (!ident.smimeSigningKey().isEmpty()) { if (ident.smimeSigningKey() != ident.smimeEncryptionKey()) { auto const key = mKeyCache->findByKeyIDOrFingerprint(ident.smimeSigningKey().constData()); if (key.isNull() || !key.canSign()) { mNearExpiryWarning->addInfo(i18nc("The argument is as SMIME fingerprint", "Your selected SMIME signing key (%1) doesn't exist in your keyring or is not suitable for signing.", QString::fromUtf8(ident.smimeSigningKey()))); mNearExpiryWarning->setWarning(true); mNearExpiryWarning->show(); } else { mComposerBase->expiryChecker()->checkKey(key, Kleo::ExpiryChecker::OwnSigningKey); } } } } } void ComposerWindow::slotCursorPositionChanged() { // Change Line/Column info in status bar const int line = mComposerBase->editor()->linePosition() + 1; const int col = mComposerBase->editor()->columnNumber() + 1; QString temp = i18nc("Shows the linenumber of the cursor position.", " Line: %1 ", line); mCursorLineLabel->setText(temp); temp = i18n(" Column: %1 ", col); mCursorColumnLabel->setText(temp); // Show link target in status bar if (mComposerBase->editor()->textCursor().charFormat().isAnchor()) { const QString text = mComposerBase->editor()->composerControler()->currentLinkText() + QLatin1String(" -> ") + mComposerBase->editor()->composerControler()->currentLinkUrl(); mStatusbarLabel->setText(text); } else { mStatusbarLabel->clear(); } } KIdentityManagementCore::Identity ComposerWindow::identity() const { return mIdentity; } QString ComposerWindow::subject() const { return mEdtSubject->text(); } QString ComposerWindow::content() const { return mComposerBase->editor()->toCleanHtml(); } RecipientsEditor *ComposerWindow::recipientsEditor() const { return mRecipientEditor; } void ComposerWindow::addAttachment(const QList &infos, bool showWarning) { QStringList lst; for (const AttachmentInfo &info : infos) { if (showWarning) { lst.append(info.url.toDisplayString()); } mComposerBase->addAttachment(info.url, info.comment, false); } if (showWarning) { // TODO // mAttachmentFromExternalMissing->setAttachmentNames(lst); // mAttachmentFromExternalMissing->animatedShow(); } } void ComposerWindow::addAttachment(const QString &name, KMime::Headers::contentEncoding cte, const QString &charset, const QByteArray &data, const QByteArray &mimeType) { Q_UNUSED(cte) mComposerBase->addAttachment(name, name, charset, data, mimeType); } void ComposerWindow::insertUrls(const QMimeData *source, const QList &urlList) { QStringList urlAdded; for (const QUrl &url : urlList) { QString urlStr; if (url.scheme() == QLatin1String("mailto")) { urlStr = KEmailAddress::decodeMailtoUrl(url); } else { urlStr = url.toDisplayString(); // Workaround #346370 if (urlStr.isEmpty()) { urlStr = source->text(); } } if (!urlAdded.contains(urlStr)) { mComposerBase->editor()->composerControler()->insertLink(urlStr); urlAdded.append(urlStr); } } } bool ComposerWindow::insertFromMimeData(const QMimeData *source, bool forceAttachment) { // If this is a PNG image, either add it as an attachment or as an inline image if (source->hasHtml() && mComposerBase->editor()->textMode() == MessageComposer::RichTextComposerNg::Rich) { const QString html = QString::fromUtf8(source->data(QStringLiteral("text/html"))); mComposerBase->editor()->insertHtml(html); return true; } else if (source->hasHtml() && (mComposerBase->editor()->textMode() == MessageComposer::RichTextComposerNg::Plain) && source->hasText() && !forceAttachment) { mComposerBase->editor()->insertPlainText(source->text()); return true; } else if (source->hasImage() && source->hasFormat(QStringLiteral("image/png"))) { // Get the image data before showing the dialog, since that processes events which can delete // the QMimeData object behind our back const QByteArray imageData = source->data(QStringLiteral("image/png")); if (imageData.isEmpty()) { return true; } if (!forceAttachment) { if (mComposerBase->editor()->textMode() == MessageComposer::RichTextComposerNg::Rich /*&& mComposerBase->editor()->isEnableImageActions() Necessary ?*/) { auto image = qvariant_cast(source->imageData()); QFileInfo fi(source->text()); QMenu menu(this); const QAction *addAsInlineImageAction = menu.addAction(i18n("Add as &Inline Image")); menu.addAction(i18n("Add as &Attachment")); const QAction *selectedAction = menu.exec(QCursor::pos()); if (selectedAction == addAsInlineImageAction) { // Let the textedit from kdepimlibs handle inline images mComposerBase->editor()->composerControler()->composerImages()->insertImage(image, fi); return true; } else if (!selectedAction) { return true; } // else fall through } } // Ok, when we reached this point, the user wants to add the image as an attachment. // Ask for the filename first. bool ok; QString attName = QInputDialog::getText(this, i18n("KMail"), i18n("Name of the attachment:"), QLineEdit::Normal, QString(), &ok); if (!ok) { return true; } attName = attName.trimmed(); if (attName.isEmpty()) { KMessageBox::error(this, i18n("Attachment name can't be empty"), i18nc("@title:window", "Invalid Attachment Name")); return true; } addAttachment(attName, KMime::Headers::CEbase64, QString(), imageData, "image/png"); return true; } else { auto job = new DndFromArkJob(this); job->setComposerWindow(this); if (job->extract(source)) { return true; } } // If this is a URL list, add those files as attachments or text // but do not offer this if we are pasting plain text containing an url, e.g. from a browser const QList urlList = source->urls(); if (!urlList.isEmpty()) { // Search if it's message items. bool allLocalURLs = true; for (const QUrl &url : urlList) { if (!url.isLocalFile()) { allLocalURLs = false; } } if (allLocalURLs || forceAttachment) { QList infoList; infoList.reserve(urlList.count()); for (const QUrl &url : urlList) { AttachmentInfo info; info.url = url; infoList.append(info); } addAttachment(infoList, false); } else { QMenu p; const int sizeUrl(urlList.size()); const QAction *addAsTextAction = p.addAction(i18np("Add URL into Message", "Add URLs into Message", sizeUrl)); const QAction *addAsAttachmentAction = p.addAction(i18np("Add File as &Attachment", "Add Files as &Attachment", sizeUrl)); const QAction *selectedAction = p.exec(QCursor::pos()); if (selectedAction == addAsTextAction) { insertUrls(source, urlList); } else if (selectedAction == addAsAttachmentAction) { QList infoList; for (const QUrl &url : urlList) { if (url.isValid()) { AttachmentInfo info; info.url = url; infoList.append(info); } } addAttachment(infoList, false); } } return true; } return false; } void ComposerWindow::slotSaveDraft() { mComposerBase->autoSaveMessage(); } void ComposerWindow::slotSaveAsFile() { auto job = new SaveAsFileJob(this); job->setParentWidget(this); job->setHtmlMode(mComposerBase->editor()->textMode() == MessageComposer::RichTextComposerNg::Rich); job->setTextDocument(mComposerBase->editor()->document()); job->start(); } QUrl ComposerWindow::insertFile() { const auto fileName = QFileDialog::getOpenFileName(this, i18nc("@title:window", "Insert File")); return QUrl::fromUserInput(fileName); } void ComposerWindow::slotInsertFile() { const QUrl u = insertFile(); if (u.isEmpty()) { return; } mRecentAction->addUrl(u); // Prevent race condition updating list when multiple composers are open { QUrlQuery query(u); QStringList urls = MessageComposer::MessageComposerSettings::self()->recentUrls(); // Prevent config file from growing without bound // Would be nicer to get this constant from KRecentFilesAction const int mMaxRecentFiles = 30; while (urls.count() > mMaxRecentFiles) { urls.removeLast(); } urls.prepend(u.toDisplayString()); MessageComposer::MessageComposerSettings::self()->setRecentUrls(urls); MessageComposer::MessageComposerSettings::self()->save(); } slotInsertRecentFile(u); } void ComposerWindow::slotRecentListFileClear() { MessageComposer::MessageComposerSettings::self()->setRecentUrls({}); MessageComposer::MessageComposerSettings::self()->save(); } void ComposerWindow::slotInsertRecentFile(const QUrl &u) { if (u.fileName().isEmpty()) { return; } auto job = new MessageComposer::InsertTextFileJob(mComposerBase->editor(), u); job->start(); } void ComposerWindow::slotPrint() { QPrinter printer; QPrintDialog dialog(&printer, this); dialog.setWindowTitle(i18nc("@title:window", "Print Document")); if (dialog.exec() != QDialog::Accepted) return; printInternal(&printer); } void ComposerWindow::slotPrintPreview() { auto dialog = new QPrintPreviewDialog(this); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->resize(800, 750); dialog->setWindowTitle(i18nc("@title:window", "Print Document")); QObject::connect(dialog, &QPrintPreviewDialog::paintRequested, this, [this](QPrinter *printer) { printInternal(printer); }); dialog->open(); } void ComposerWindow::printInternal(QPrinter *printer) { mComposerBase->setFrom(mFrom); mComposerBase->setSubject(mEdtSubject->text()); mComposerBase->generateMessage([printer](const QList &messages) { if (messages.isEmpty()) { return; } MimeTreeParser::Widgets::MessageViewer messageViewer; messageViewer.setMessage(messages[0]); QPainter painter; painter.begin(printer); const auto pageLayout = printer->pageLayout(); const auto pageRect = pageLayout.paintRectPixels(printer->resolution()); const double xscale = pageRect.width() / double(messageViewer.width()); const double yscale = pageRect.height() / double(messageViewer.height()); const double scale = qMin(qMin(xscale, yscale), 1.); painter.translate(pageRect.x(), pageRect.y()); painter.scale(scale, scale); messageViewer.print(&painter, pageRect.width()); }); } void ComposerWindow::slotPasteAsAttachment() { const QMimeData *mimeData = QApplication::clipboard()->mimeData(); if (!mimeData) { return; } if (insertFromMimeData(mimeData, true)) { return; } if (mimeData->hasText()) { bool ok; const QString attName = QInputDialog::getText(this, i18n("Insert clipboard text as attachment"), i18n("Name of the attachment:"), QLineEdit::Normal, QString(), &ok); if (ok) { mComposerBase->addAttachment(attName, attName, QStringLiteral("utf-8"), QApplication::clipboard()->text().toUtf8(), "text/plain"); } return; } } void ComposerWindow::slotWordWrapToggled(bool on) { if (on) { mComposerBase->editor()->enableWordWrap(validateLineWrapWidth()); } else { disableWordWrap(); } } int ComposerWindow::validateLineWrapWidth() const { int lineWrap = MessageComposer::MessageComposerSettings::self()->lineWrapWidth(); if ((lineWrap == 0) || (lineWrap > 78)) { lineWrap = 78; } else if (lineWrap < 30) { lineWrap = 30; } return lineWrap; } void ComposerWindow::disableWordWrap() { mComposerBase->editor()->disableWordWrap(); } void ComposerWindow::slotAutoSpellCheckingToggled(bool enabled) { mAutoSpellCheckingAction->setChecked(enabled); if (mComposerBase->editor()->checkSpellingEnabled() != enabled) { mComposerBase->editor()->setCheckSpellingEnabled(enabled); } //mStatusBarLabelSpellCheckingChangeMode->setToggleMode(enabled); } void ComposerWindow::slotSpellcheckConfig() { QPointer dialog = new SpellCheckerConfigDialog(this); if (!mComposerBase->editor()->spellCheckingLanguage().isEmpty()) { dialog->setLanguage(mComposerBase->editor()->spellCheckingLanguage()); } if (dialog->exec()) { mComposerBase->editor()->setSpellCheckingLanguage(dialog->language()); } delete dialog; } void ComposerWindow::closeEvent(QCloseEvent *event) { event->ignore(); ComposerWindowFactory::self().clear(this); } bool ComposerWindow::queryClose() { return KXmlGuiWindow::queryClose(); }