diff --git a/client/draft/draftmanager.cpp b/client/draft/draftmanager.cpp index 02395c4..3cd8074 100644 --- a/client/draft/draftmanager.cpp +++ b/client/draft/draftmanager.cpp @@ -1,78 +1,89 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "draftmanager.h" #include #include #include "editor_debug.h" DraftManager::DraftManager(bool testMode) : m_testMode(testMode) { const QDir directory(draftDirectory(testMode)); const auto entries = directory.entryList(QDir::Files); for (const QString &entry : entries) { Draft draft(draftDirectory() + entry); if (draft.isValid()) { m_drafts << draft; } else { qFatal(EDITOR_LOG) << "File does not exist or is not readable" << entry ; } } } QString DraftManager::draftDirectory(bool testMode) +{ + if (testMode) { + static const QString path = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + QLatin1String("/gpgol-server/draft/"); + return path; + } else { + static const QString path = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/gpgol-server/draft/"); + return path; + } +} + +QString DraftManager::autosaveDirectory(bool testMode) { if (testMode) { static const QString path = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + QLatin1String("/gpgol-server/autosave/"); return path; } else { static const QString path = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/gpgol-server/autosave/"); return path; } } DraftManager &DraftManager::self(bool testMode) { static DraftManager s_draftManager(testMode); return s_draftManager; } QList DraftManager::drafts() const { return m_drafts; } QJsonArray DraftManager::toJson() const { if (m_drafts.isEmpty()) { return {}; } QJsonArray array; std::transform(m_drafts.cbegin(), m_drafts.cend(), std::back_inserter(array), [](const auto draft) { return draft.toJson(); }); return array; } bool DraftManager::remove(const Draft &draft) { auto it = std::find(m_drafts.begin(), m_drafts.end(), draft); if (it == m_drafts.end()) { return false; } bool ok = it->remove(); m_drafts.erase(it); return ok; } Draft DraftManager::draftById(const QByteArray &draftId) { return Draft(draftDirectory() + QString::fromUtf8(draftId)); } diff --git a/client/draft/draftmanager.h b/client/draft/draftmanager.h index b55bcef..cdeb25f 100644 --- a/client/draft/draftmanager.h +++ b/client/draft/draftmanager.h @@ -1,39 +1,43 @@ // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #pragma once #include "draft.h" #include /// @class DraftManager /// /// Manage the email drafts of the user. class DraftManager { public: /// Get the DraftManager singleton. /// \param testMode Set it to true when running unit tests. static DraftManager &self(bool testMode = false); /// Get the directory where the drafts are stored. /// \param testMode Set it to true when running unit tests. static QString draftDirectory(bool testMode = false); + /// Get the directory where the autosaved drafts are stored. + /// \param testMode Set it to true when running unit tests. + static QString autosaveDirectory(bool testMode = false); + /// List of drafts. [[nodiscard]] QList drafts() const; /// List of drafts as JSON array. [[nodiscard]] QJsonArray toJson() const; /// Remove the specified dradt from the filesystem. bool remove(const Draft &draft); /// Get a draft by it's id. Draft draftById(const QByteArray &draftId); private: DraftManager(bool testMode); QList m_drafts; bool m_testMode = false; }; \ No newline at end of file diff --git a/client/editor/composer.cpp b/client/editor/composer.cpp index 6b09bd6..29298b7 100644 --- a/client/editor/composer.cpp +++ b/client/editor/composer.cpp @@ -1,671 +1,686 @@ /* SPDX-FileCopyrightText: 2009 Constantin Berzan SPDX-FileCopyrightText: 2009 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.net SPDX-FileCopyrightText: 2009 Leo Franchi SPDX-License-Identifier: LGPL-2.0-or-later */ #include "composer.h" #include "job/attachmentjob.h" #include "job/encryptjob.h" #include "job/itipjob.h" #include "job/jobbase_p.h" #include "job/maintextjob.h" #include "job/multipartjob.h" #include "job/signencryptjob.h" #include "job/signjob.h" #include "job/skeletonmessagejob.h" #include "job/transparentjob.h" #include "part/globalpart.h" #include "part/infopart.h" #include "part/itippart.h" #include "part/textpart.h" #include "editor_debug.h" #include using namespace MessageComposer; using MessageCore::AttachmentPart; class MessageComposer::ComposerPrivate : public JobBasePrivate { public: explicit ComposerPrivate(Composer *qq) : JobBasePrivate(qq) { } ~ComposerPrivate() override { delete skeletonMessage; } void init(); void doStart(); // slot void composeStep1(); void composeStep2(); [[nodiscard]] QList createEncryptJobs(ContentJobBase *contentJob, bool sign); void contentJobFinished(KJob *job); // slot void composeWithLateAttachments(KMime::Message *headers, KMime::Content *content, const AttachmentPart::List &parts, const std::vector &keys, const QStringList &recipients); void attachmentsFinished(KJob *job); // slot void composeFinalStep(KMime::Content *headers, KMime::Content *content); QString gnupgHome; QList>> encData; GpgME::Key senderEncryptionKey; std::vector signers; AttachmentPart::List attachmentParts; // attachments with different sign/encrypt settings from // main message body. added at the end of the process AttachmentPart::List lateAttachmentParts; QList resultMessages; Kleo::CryptoMessageFormat format; // Stuff that the application plays with. GlobalPart *globalPart = nullptr; InfoPart *infoPart = nullptr; TextPart *textPart = nullptr; ItipPart *itipPart = nullptr; // Stuff that we play with. KMime::Message *skeletonMessage = nullptr; bool started = false; bool finished = false; bool sign = false; bool encrypt = false; bool noCrypto = false; bool autoSaving = false; + bool draft = false; Q_DECLARE_PUBLIC(Composer) }; void ComposerPrivate::init() { Q_Q(Composer); // We cannot create these in ComposerPrivate's constructor, because // their parent q is not fully constructed at that time. globalPart = new GlobalPart(q); infoPart = new InfoPart(q); textPart = nullptr; itipPart = nullptr; } void ComposerPrivate::doStart() { Q_ASSERT(!started); started = true; composeStep1(); } void ComposerPrivate::composeStep1() { Q_Q(Composer); // Create skeleton message (containing headers only; no content). auto skeletonJob = new SkeletonMessageJob(infoPart, globalPart, q); QObject::connect(skeletonJob, &SkeletonMessageJob::finished, q, [this, skeletonJob](KJob *job) { if (job->error()) { return; // KCompositeJob takes care of the error. } // SkeletonMessageJob is a special job creating a Message instead of a Content. Q_ASSERT(skeletonMessage == nullptr); skeletonMessage = skeletonJob->message(); Q_ASSERT(skeletonMessage); skeletonMessage->assemble(); composeStep2(); }); q->addSubjob(skeletonJob); skeletonJob->start(); } void ComposerPrivate::composeStep2() { Q_Q(Composer); ContentJobBase *mainJob = nullptr; ContentJobBase *mainContentJob = nullptr; Q_ASSERT(textPart || itipPart); // At least one must be present, otherwise it's a useless message if (textPart && !itipPart) { mainContentJob = new MainTextJob(textPart, q); } else if (!textPart && itipPart) { mainContentJob = new ItipJob(itipPart, q); } else { // Combination of both text and itip parts not supported right now Q_ASSERT(!textPart || !itipPart); } if ((sign || encrypt) && format & Kleo::InlineOpenPGPFormat) { // needs custom handling --- one SignEncryptJob by itself qCDebug(EDITOR_LOG) << "sending to sign/enc inline job!"; if (encrypt) { // TODO: fix Inline PGP with encrypted attachments const QList jobs = createEncryptJobs(mainContentJob, sign); for (ContentJobBase *subJob : jobs) { if (attachmentParts.isEmpty()) { // We have no attachments. Use the content given by the mainContentJob mainJob = subJob; } else { auto multipartJob = new MultipartJob(q); multipartJob->setMultipartSubtype("mixed"); multipartJob->appendSubjob(subJob); for (const AttachmentPart::Ptr &part : std::as_const(attachmentParts)) { multipartJob->appendSubjob(new AttachmentJob(part)); } mainJob = multipartJob; } QObject::connect(mainJob, SIGNAL(finished(KJob *)), q, SLOT(contentJobFinished(KJob *))); q->addSubjob(mainJob); } } else { auto subJob = new SignJob(q); subJob->setSigningKeys(signers); subJob->setCryptoMessageFormat(format); subJob->appendSubjob(mainContentJob); if (attachmentParts.isEmpty()) { // We have no attachments. Use the content given by the mainContentJob. mainJob = subJob; } else { auto multipartJob = new MultipartJob(q); multipartJob->setMultipartSubtype("mixed"); multipartJob->appendSubjob(subJob); for (const AttachmentPart::Ptr &part : std::as_const(attachmentParts)) { multipartJob->appendSubjob(new AttachmentJob(part)); } mainJob = multipartJob; } QObject::connect(mainJob, SIGNAL(finished(KJob *)), q, SLOT(contentJobFinished(KJob *))); q->addSubjob(mainJob); } if (mainJob) { mainJob->start(); } else { qCDebug(EDITOR_LOG) << "main job is null"; } return; } if (attachmentParts.isEmpty()) { // We have no attachments. Use the content given by the mainContentJob mainJob = mainContentJob; } else { // We have attachments. Create a multipart/mixed content. QMutableListIterator iter(attachmentParts); while (iter.hasNext()) { AttachmentPart::Ptr part = iter.next(); qCDebug(EDITOR_LOG) << "Checking attachment crypto policy... signed: " << part->isSigned() << " isEncrypted : " << part->isEncrypted(); - if (!noCrypto && !autoSaving && (sign != part->isSigned() || encrypt != part->isEncrypted())) { // different policy + if (!noCrypto && !autoSaving && !draft && (sign != part->isSigned() || encrypt != part->isEncrypted())) { // different policy qCDebug(EDITOR_LOG) << "got attachment with different crypto policy!"; lateAttachmentParts.append(part); iter.remove(); } } auto multipartJob = new MultipartJob(q); multipartJob->setMultipartSubtype("mixed"); multipartJob->appendSubjob(mainContentJob); for (const AttachmentPart::Ptr &part : std::as_const(attachmentParts)) { multipartJob->appendSubjob(new AttachmentJob(part)); } mainJob = multipartJob; } if (sign) { auto sJob = new SignJob(q); sJob->setCryptoMessageFormat(format); sJob->setSigningKeys(signers); sJob->appendSubjob(mainJob); sJob->setSkeletonMessage(skeletonMessage); mainJob = sJob; } if (encrypt) { const auto lstJob = createEncryptJobs(mainJob, false); for (ContentJobBase *job : lstJob) { auto eJob = dynamic_cast(job); if (eJob && sign) { // When doing Encrypt and Sign move headers only in the signed part eJob->setProtectedHeaders(false); } QObject::connect(job, SIGNAL(finished(KJob *)), q, SLOT(contentJobFinished(KJob *))); q->addSubjob(job); mainJob = job; // start only last EncryptJob } } else { QObject::connect(mainJob, SIGNAL(finished(KJob *)), q, SLOT(contentJobFinished(KJob *))); q->addSubjob(mainJob); } mainJob->start(); } QList ComposerPrivate::createEncryptJobs(ContentJobBase *contentJob, bool sign) { Q_Q(Composer); QList jobs; // each SplitInfo holds a list of recipients/keys, if there is more than // one item in it then it means there are secondary recipients that need // different messages w/ clean headers qCDebug(EDITOR_LOG) << "starting enc jobs"; qCDebug(EDITOR_LOG) << "format:" << format; qCDebug(EDITOR_LOG) << "enc data:" << encData.size(); if (encData.isEmpty()) { // no key data! bail! q->setErrorText(i18n("No key data for recipients found.")); q->setError(Composer::IncompleteError); q->emitResult(); return jobs; } const int encDataSize = encData.size(); jobs.reserve(encDataSize); for (int i = 0; i < encDataSize; ++i) { QPair> recipients = encData[i]; qCDebug(EDITOR_LOG) << "got first list of recipients:" << recipients.first; ContentJobBase *subJob = nullptr; if (sign) { auto seJob = new SignEncryptJob(q); seJob->setCryptoMessageFormat(format); seJob->setSigningKeys(signers); seJob->setEncryptionKeys(recipients.second); seJob->setRecipients(recipients.first); seJob->setSkeletonMessage(skeletonMessage); subJob = seJob; } else { auto eJob = new EncryptJob(q); eJob->setCryptoMessageFormat(format); eJob->setEncryptionKeys(recipients.second); eJob->setRecipients(recipients.first); eJob->setSkeletonMessage(skeletonMessage); eJob->setGnupgHome(gnupgHome); subJob = eJob; } qCDebug(EDITOR_LOG) << "subJob" << subJob; subJob->appendSubjob(contentJob); jobs.append(subJob); } qCDebug(EDITOR_LOG) << jobs.size(); return jobs; } void ComposerPrivate::contentJobFinished(KJob *job) { if (job->error()) { return; // KCompositeJob takes care of the error. } qCDebug(EDITOR_LOG) << "composing final message"; KMime::Message *headers = nullptr; KMime::Content *resultContent = nullptr; std::vector keys; QStringList recipients; Q_ASSERT(dynamic_cast(job) == static_cast(job)); auto contentJob = static_cast(job); // create the final headers and body, // taking into account secondary recipients for encryption if (encData.size() > 1) { // crypto job with secondary recipients.. Q_ASSERT(dynamic_cast(job)); // we need to get the recipients for this job auto eJob = dynamic_cast(job); keys = eJob->encryptionKeys(); recipients = eJob->recipients(); resultContent = contentJob->content(); // content() comes from superclass headers = new KMime::Message; headers->setHeader(skeletonMessage->from()); headers->setHeader(skeletonMessage->to()); headers->setHeader(skeletonMessage->cc()); headers->setHeader(skeletonMessage->subject()); headers->setHeader(skeletonMessage->date()); headers->setHeader(skeletonMessage->messageID()); auto realTo = new KMime::Headers::Generic("X-KMail-EncBccRecipients"); realTo->fromUnicodeString(eJob->recipients().join(QLatin1Char('%')), "utf-8"); qCDebug(EDITOR_LOG) << "got one of multiple messages sending to:" << realTo->asUnicodeString(); qCDebug(EDITOR_LOG) << "sending to recipients:" << recipients; headers->setHeader(realTo); headers->assemble(); } else { // just use the saved headers from before if (!encData.isEmpty()) { const auto firstElement = encData.at(0); qCDebug(EDITOR_LOG) << "setting enc data:" << firstElement.first << "with num keys:" << firstElement.second.size(); keys = firstElement.second; recipients = firstElement.first; } headers = skeletonMessage; resultContent = contentJob->content(); } if (lateAttachmentParts.isEmpty()) { composeFinalStep(headers, resultContent); } else { composeWithLateAttachments(headers, resultContent, lateAttachmentParts, keys, recipients); } } void ComposerPrivate::composeWithLateAttachments(KMime::Message *headers, KMime::Content *content, const AttachmentPart::List &parts, const std::vector &keys, const QStringList &recipients) { Q_Q(Composer); auto multiJob = new MultipartJob(q); multiJob->setMultipartSubtype("mixed"); // wrap the content into a job for the multijob to handle it auto tJob = new MessageComposer::TransparentJob(q); tJob->setContent(content); multiJob->appendSubjob(tJob); multiJob->setExtraContent(headers); qCDebug(EDITOR_LOG) << "attachment encr key size:" << keys.size() << " recipients: " << recipients; // operate correctly on each attachment that has a different crypto policy than body. for (const AttachmentPart::Ptr &attachment : std::as_const(parts)) { auto attachJob = new AttachmentJob(attachment, q); qCDebug(EDITOR_LOG) << "got a late attachment"; if (attachment->isSigned() && format) { qCDebug(EDITOR_LOG) << "adding signjob for late attachment"; auto sJob = new SignJob(q); sJob->setContent(nullptr); sJob->setCryptoMessageFormat(format); sJob->setSigningKeys(signers); sJob->appendSubjob(attachJob); if (attachment->isEncrypted()) { qCDebug(EDITOR_LOG) << "adding sign + encrypt job for late attachment"; auto eJob = new EncryptJob(q); eJob->setCryptoMessageFormat(format); eJob->setEncryptionKeys(keys); eJob->setRecipients(recipients); eJob->appendSubjob(sJob); multiJob->appendSubjob(eJob); } else { qCDebug(EDITOR_LOG) << "Just signing late attachment"; multiJob->appendSubjob(sJob); } } else if (attachment->isEncrypted() && format) { // only encryption qCDebug(EDITOR_LOG) << "just encrypting late attachment"; auto eJob = new EncryptJob(q); eJob->setCryptoMessageFormat(format); eJob->setEncryptionKeys(keys); eJob->setRecipients(recipients); eJob->appendSubjob(attachJob); multiJob->appendSubjob(eJob); } else { qCDebug(EDITOR_LOG) << "attaching plain non-crypto attachment"; auto attachSecondJob = new AttachmentJob(attachment, q); multiJob->appendSubjob(attachSecondJob); } } QObject::connect(multiJob, SIGNAL(finished(KJob *)), q, SLOT(attachmentsFinished(KJob *))); q->addSubjob(multiJob); multiJob->start(); } void ComposerPrivate::attachmentsFinished(KJob *job) { if (job->error()) { return; // KCompositeJob takes care of the error. } qCDebug(EDITOR_LOG) << "composing final message with late attachments"; Q_ASSERT(dynamic_cast(job)); auto contentJob = static_cast(job); KMime::Content *content = contentJob->content(); KMime::Content *headers = contentJob->extraContent(); composeFinalStep(headers, content); } void ComposerPrivate::composeFinalStep(KMime::Content *headers, KMime::Content *content) { content->assemble(); const QByteArray allData = headers->head() + content->encodedContent(); delete content; KMime::Message::Ptr resultMessage(new KMime::Message); resultMessage->setContent(allData); resultMessage->parse(); // Not strictly necessary. resultMessages.append(resultMessage); } Composer::Composer(QObject *parent) : JobBase(*new ComposerPrivate(this), parent) { Q_D(Composer); d->init(); } Composer::~Composer() = default; QList Composer::resultMessages() const { Q_D(const Composer); Q_ASSERT(d->finished); Q_ASSERT(!error()); return d->resultMessages; } GlobalPart *Composer::globalPart() const { Q_D(const Composer); return d->globalPart; } InfoPart *Composer::infoPart() const { Q_D(const Composer); return d->infoPart; } TextPart *Composer::textPart() const { Q_D(const Composer); if (!d->textPart) { auto *self = const_cast(this); self->d_func()->textPart = new TextPart(self); } return d->textPart; } void Composer::clearTextPart() { Q_D(Composer); delete d->textPart; d->textPart = nullptr; } ItipPart *Composer::itipPart() const { Q_D(const Composer); if (!d->itipPart) { auto *self = const_cast(this); self->d_func()->itipPart = new ItipPart(self); } return d->itipPart; } void Composer::clearItipPart() { Q_D(Composer); delete d->itipPart; d->itipPart = nullptr; } AttachmentPart::List Composer::attachmentParts() const { Q_D(const Composer); return d->attachmentParts; } void Composer::addAttachmentPart(AttachmentPart::Ptr part, bool autoresizeImage) { Q_D(Composer); Q_ASSERT(!d->started); Q_ASSERT(!d->attachmentParts.contains(part)); if (autoresizeImage) { //MessageComposer::Utils resizeUtils; //if (resizeUtils.resizeImage(part)) { // MessageComposer::ImageScaling autoResizeJob; // autoResizeJob.setName(part->name()); // autoResizeJob.setMimetype(part->mimeType()); // if (autoResizeJob.loadImageFromData(part->data())) { // if (autoResizeJob.resizeImage()) { // part->setData(autoResizeJob.imageArray()); // part->setMimeType(autoResizeJob.mimetype()); // part->setName(autoResizeJob.generateNewName()); // resizeUtils.changeFileName(part); // } // } //} } d->attachmentParts.append(part); } void Composer::addAttachmentParts(const AttachmentPart::List &parts, bool autoresizeImage) { for (const AttachmentPart::Ptr &part : parts) { addAttachmentPart(part, autoresizeImage); } } void Composer::removeAttachmentPart(AttachmentPart::Ptr part) { Q_D(Composer); Q_ASSERT(!d->started); const int numberOfElements = d->attachmentParts.removeAll(part); if (numberOfElements <= 0) { qCCritical(EDITOR_LOG) << "Unknown attachment part" << part.data(); Q_ASSERT(false); return; } } void Composer::setSignAndEncrypt(const bool doSign, const bool doEncrypt) { Q_D(Composer); d->sign = doSign; d->encrypt = doEncrypt; } void Composer::setMessageCryptoFormat(Kleo::CryptoMessageFormat format) { Q_D(Composer); d->format = format; } void Composer::setSigningKeys(const std::vector &signers) { Q_D(Composer); d->signers = signers; } void Composer::setEncryptionKeys(const QList>> &encData) { Q_D(Composer); d->encData = encData; } void Composer::setNoCrypto(bool noCrypto) { Q_D(Composer); d->noCrypto = noCrypto; } void Composer::setSenderEncryptionKey(const GpgME::Key &senderKey) { Q_D(Composer); d->senderEncryptionKey = senderKey; } void Composer::setGnupgHome(const QString &path) { Q_D(Composer); d->gnupgHome = path; } QString Composer::gnupgHome() const { Q_D(const Composer); return d->gnupgHome; } bool Composer::finished() const { Q_D(const Composer); return d->finished; } bool Composer::autoSave() const { Q_D(const Composer); return d->autoSaving; } void Composer::setAutoSave(bool isAutoSave) { Q_D(Composer); d->autoSaving = isAutoSave; } +bool Composer::draft() const +{ + Q_D(const Composer); + + return d->draft; +} + +void Composer::setDraft(bool draft) +{ + Q_D(Composer); + + d->draft = draft; +} + void Composer::start() { Q_D(Composer); d->doStart(); } void Composer::slotResult(KJob *job) { Q_D(Composer); JobBase::slotResult(job); if (!hasSubjobs()) { d->finished = true; emitResult(); } } #include "moc_composer.cpp" diff --git a/client/editor/composer.h b/client/editor/composer.h index ff6f142..adeef29 100644 --- a/client/editor/composer.h +++ b/client/editor/composer.h @@ -1,84 +1,89 @@ // SPDX-FileCopyrightText: 2009 Constantin Berzan // SPDX-FileCopyrightText: 2023 g10 code GmbH // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: LGPL-2.0-or-later #pragma once #include #include #include #include #include #include #include "attachment/attachmentpart.h" #include "job/jobbase.h" namespace MessageComposer { class ComposerPrivate; class GlobalPart; class InfoPart; class TextPart; class ItipPart; class Composer : public JobBase { Q_OBJECT public: explicit Composer(QObject *parent = nullptr); ~Composer() override; [[nodiscard]] QList resultMessages() const; [[nodiscard]] GlobalPart *globalPart() const; [[nodiscard]] InfoPart *infoPart() const; [[nodiscard]] TextPart *textPart() const; [[nodiscard]] ItipPart *itipPart() const; void clearTextPart(); void clearItipPart(); [[nodiscard]] MessageCore::AttachmentPart::List attachmentParts() const; void addAttachmentPart(MessageCore::AttachmentPart::Ptr part, bool autoresizeImage = false); void addAttachmentParts(const MessageCore::AttachmentPart::List &parts, bool autoresizeImage = false); void removeAttachmentPart(MessageCore::AttachmentPart::Ptr part); // if the message and attachments should not be encrypted regardless of settings void setNoCrypto(bool noCrypto); void setSignAndEncrypt(const bool doSign, const bool doEncrypt); void setMessageCryptoFormat(Kleo::CryptoMessageFormat format); void setSigningKeys(const std::vector &signers); void setEncryptionKeys(const QList>> &data); void setSenderEncryptionKey(const GpgME::Key &senderKey); void setGnupgHome(const QString &path); [[nodiscard]] QString gnupgHome() const; /// Sets if this message being composed is an auto-saved message. /// If so, it will be handled differently like without support for crypto attachment. void setAutoSave(bool isAutoSave); [[nodiscard]] bool autoSave() const; + /// Sets if this message being composed is a draft message. + /// If so, it will be handled differently like with only encrypted for the sender. + void setDraft(bool isAutoSave); + [[nodiscard]] bool draft() const; + [[nodiscard]] bool finished() const; public Q_SLOTS: void start() override; protected Q_SLOTS: void slotResult(KJob *job) override; private: Q_DECLARE_PRIVATE(Composer) Q_PRIVATE_SLOT(d_func(), void doStart()) Q_PRIVATE_SLOT(d_func(), void contentJobFinished(KJob *)) Q_PRIVATE_SLOT(d_func(), void attachmentsFinished(KJob *)) }; } diff --git a/client/editor/composerviewbase.cpp b/client/editor/composerviewbase.cpp index eb27423..556aaeb 100644 --- a/client/editor/composerviewbase.cpp +++ b/client/editor/composerviewbase.cpp @@ -1,1355 +1,1455 @@ /* SPDX-FileCopyrightText: 2010 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.com SPDX-FileCopyrightText: 2010 Leo Franchi SPDX-License-Identifier: LGPL-2.0-or-later */ #include "composerviewbase.h" #include "attachment/attachmentcontrollerbase.h" #include "attachment/attachmentmodel.h" #include "richtextcomposerng.h" #include "richtextcomposersignatures.h" #include "composer.h" #include "nodehelper.h" #include "signaturecontroller.h" #include "part/globalpart.h" #include "part/infopart.h" #include "util.h" #include "util_p.h" #include "ews/ewsmailfactory.h" #include "mailtemplates.h" #include "../qnam.h" #include "messagecomposersettings.h" #include "recipientseditor.h" #include #include "identity/identity.h" #include #include #include #include #include #include "editor_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "draft/draftmanager.h" using namespace MessageComposer; using namespace Qt::Literals::StringLiterals; ComposerViewBase::ComposerViewBase(QObject *parent, QWidget *parentGui) : QObject(parent) , m_msg(KMime::Message::Ptr(new KMime::Message)) , m_parentWidget(parentGui) , m_cryptoMessageFormat(Kleo::AutoFormat) , m_autoSaveInterval(60000) // default of 1 min { m_charsets << "utf-8"; // default, so we have a backup in case client code forgot to set. initAutoSave(); connect(this, &ComposerViewBase::composerCreated, this, &ComposerViewBase::slotComposerCreated); } ComposerViewBase::~ComposerViewBase() = default; bool ComposerViewBase::isComposing() const { return !m_composers.isEmpty(); } void ComposerViewBase::setMessage(const KMime::Message::Ptr &msg) { if (m_attachmentModel) { const auto attachments{m_attachmentModel->attachments()}; for (const MessageCore::AttachmentPart::Ptr &attachment : attachments) { if (!m_attachmentModel->removeAttachment(attachment)) { qCWarning(EDITOR_LOG) << "Attachment not found."; } } } m_msg = msg; if (m_recipientsEditor) { m_recipientsEditor->clear(); bool resultTooManyRecipients = m_recipientsEditor->setRecipientString(m_msg->to()->mailboxes(), Recipient::To); if (!resultTooManyRecipients) { resultTooManyRecipients = m_recipientsEditor->setRecipientString(m_msg->cc()->mailboxes(), Recipient::Cc); } if (!resultTooManyRecipients) { resultTooManyRecipients = m_recipientsEditor->setRecipientString(m_msg->bcc()->mailboxes(), Recipient::Bcc); } if (!resultTooManyRecipients) { resultTooManyRecipients = m_recipientsEditor->setRecipientString(m_msg->replyTo()->mailboxes(), Recipient::ReplyTo); } m_recipientsEditor->setFocusBottom(); Q_EMIT tooManyRecipient(resultTooManyRecipients); } // First, we copy the message and then parse it to the object tree parser. // The otp gets the message text out of it, in textualContent(), and also decrypts // the message if necessary. auto msgContent = new KMime::Content; msgContent->setContent(m_msg->encodedContent()); msgContent->parse(); // Load the attachments const auto attachments{msgContent->attachments()}; for (const auto &att : attachments) { addAttachmentPart(att); } // Set the HTML text and collect HTML images bool isHtml = false; const auto body = MailTemplates::body(msg, isHtml); if (isHtml) { enableHtml(); } else { disableHtml(LetUserConfirm); } editor()->setText(body); if (auto hdr = m_msg->headerByType("X-KMail-CursorPos")) { m_editor->setCursorPositionFromStart(hdr->asUnicodeString().toUInt()); } delete msgContent; } void ComposerViewBase::saveMailSettings() { auto header = new KMime::Headers::Generic("X-KMail-Identity"); header->fromUnicodeString(QString::number(m_identity.uoid()), "utf-8"); m_msg->setHeader(header); header = new KMime::Headers::Generic("X-KMail-Identity-Name"); header->fromUnicodeString(m_identity.identityName(), "utf-8"); m_msg->setHeader(header); header = new KMime::Headers::Generic("X-KMail-Dictionary"); header->fromUnicodeString(m_dictionary->currentDictionary(), "utf-8"); m_msg->setHeader(header); // Save the quote prefix which is used for this message. Each message can have // a different quote prefix, for example depending on the original sender. if (m_editor->quotePrefixName().isEmpty()) { m_msg->removeHeader("X-KMail-QuotePrefix"); } else { header = new KMime::Headers::Generic("X-KMail-QuotePrefix"); header->fromUnicodeString(m_editor->quotePrefixName(), "utf-8"); m_msg->setHeader(header); } if (m_editor->composerControler()->isFormattingUsed()) { qCDebug(EDITOR_LOG) << "HTML mode"; header = new KMime::Headers::Generic("X-KMail-Markup"); header->fromUnicodeString(QStringLiteral("true"), "utf-8"); m_msg->setHeader(header); } else { m_msg->removeHeader("X-KMail-Markup"); qCDebug(EDITOR_LOG) << "Plain text"; } } void ComposerViewBase::clearFollowUp() { mFollowUpDate = QDate(); } void ComposerViewBase::send() { KCursorSaver saver(Qt::WaitCursor); saveMailSettings(); if (m_editor->composerControler()->isFormattingUsed() && inlineSigningEncryptionSelected()) { const QString keepBtnText = m_encrypt ? m_sign ? i18n("&Keep markup, do not sign/encrypt") : i18n("&Keep markup, do not encrypt") : i18n("&Keep markup, do not sign"); const QString yesBtnText = m_encrypt ? m_sign ? i18n("Sign/Encrypt (delete markup)") : i18n("Encrypt (delete markup)") : i18n("Sign (delete markup)"); int ret = KMessageBox::warningTwoActionsCancel(m_parentWidget, i18n("

Inline signing/encrypting of HTML messages is not possible;

" "

do you want to delete your markup?

"), i18nc("@title:window", "Sign/Encrypt Message?"), KGuiItem(yesBtnText), KGuiItem(keepBtnText)); if (KMessageBox::Cancel == ret) { return; } if (KMessageBox::ButtonCode::SecondaryAction == ret) { m_encrypt = false; m_sign = false; } else { Q_EMIT disableHtml(NoConfirmationNeeded); } } readyForSending(); } void ComposerViewBase::setCustomHeader(const QMap &customHeader) { m_customHeader = customHeader; } void ComposerViewBase::readyForSending() { qCDebug(EDITOR_LOG) << "Entering readyForSending"; if (!m_msg) { qCDebug(EDITOR_LOG) << "m_msg == 0!"; return; } if (!m_composers.isEmpty()) { // This may happen if e.g. the autosave timer calls applyChanges. qCDebug(EDITOR_LOG) << "ready for sending: Called while composer active; ignoring. Number of composer " << m_composers.count(); return; } mExpandedFrom = from(); mExpandedTo = m_recipientsEditor->recipientStringList(Recipient::To); mExpandedCc = m_recipientsEditor->recipientStringList(Recipient::Cc); mExpandedBcc = m_recipientsEditor->recipientStringList(Recipient::Bcc); mExpandedReplyTo = m_recipientsEditor->recipientStringList(Recipient::ReplyTo); Q_ASSERT(m_composers.isEmpty()); // composers should be empty. The caller of this function // checks for emptiness before calling it // so just ensure it actually is empty // and document it // we first figure out if we need to create multiple messages with different crypto formats // if so, we create a composer per format // if we aren't signing or encrypting, this just returns a single empty message if (m_neverEncrypt) { auto composer = new MessageComposer::Composer; composer->setNoCrypto(true); m_composers.append(composer); slotComposerCreated(); } else { generateCryptoMessages(); } } void ComposerViewBase::slotComposerCreated() { if (m_composers.isEmpty()) { Q_EMIT failed(i18n("It was not possible to create a message composer.")); return; } // Compose each message and prepare it for queueing, sending, or storing // working copy in case composers instantly emit result const auto composers = m_composers; for (MessageComposer::Composer *composer : composers) { fillComposer(composer, UseExpandedRecipients, false); connect(composer, &MessageComposer::Composer::result, this, &ComposerViewBase::slotSendComposeResult); composer->start(); qCDebug(EDITOR_LOG) << "Started a composer for sending!"; } } namespace { // helper methods for reading encryption settings inline Kleo::chrono::days encryptOwnKeyNearExpiryWarningThresholdInDays() { if (!MessageComposer::MessageComposerSettings::self()->cryptoWarnWhenNearExpire()) { return Kleo::chrono::days{-1}; } const int num = MessageComposer::MessageComposerSettings::self()->cryptoWarnOwnEncrKeyNearExpiryThresholdDays(); return Kleo::chrono::days{qMax(1, num)}; } inline Kleo::chrono::days encryptKeyNearExpiryWarningThresholdInDays() { if (!MessageComposer::MessageComposerSettings::self()->cryptoWarnWhenNearExpire()) { return Kleo::chrono::days{-1}; } const int num = MessageComposer::MessageComposerSettings::self()->cryptoWarnEncrKeyNearExpiryThresholdDays(); return Kleo::chrono::days{qMax(1, num)}; } inline Kleo::chrono::days encryptRootCertNearExpiryWarningThresholdInDays() { if (!MessageComposer::MessageComposerSettings::self()->cryptoWarnWhenNearExpire()) { return Kleo::chrono::days{-1}; } const int num = MessageComposer::MessageComposerSettings::self()->cryptoWarnEncrRootNearExpiryThresholdDays(); return Kleo::chrono::days{qMax(1, num)}; } inline Kleo::chrono::days encryptChainCertNearExpiryWarningThresholdInDays() { if (!MessageComposer::MessageComposerSettings::self()->cryptoWarnWhenNearExpire()) { return Kleo::chrono::days{-1}; } const int num = MessageComposer::MessageComposerSettings::self()->cryptoWarnEncrChaincertNearExpiryThresholdDays(); return Kleo::chrono::days{qMax(1, num)}; } inline bool showKeyApprovalDialog() { return MessageComposer::MessageComposerSettings::self()->cryptoShowKeysForApproval(); } inline bool cryptoWarningUnsigned(const KIdentityManagementCore::Identity &identity) { if (identity.encryptionOverride()) { return identity.warnNotSign(); } return MessageComposer::MessageComposerSettings::self()->cryptoWarningUnsigned(); } inline bool cryptoWarningUnencrypted(const KIdentityManagementCore::Identity &identity) { if (identity.encryptionOverride()) { return identity.warnNotEncrypt(); } return MessageComposer::MessageComposerSettings::self()->cryptoWarningUnencrypted(); } } // nameless namespace Kleo::KeyResolver *ComposerViewBase::fillKeyResolver(bool encryptSomething) { auto keyResolverCore = new Kleo::KeyResolver(m_encrypt, m_sign, GpgME::UnknownProtocol, m_encrypt); keyResolverCore->setMinimumValidity(GpgME::UserID::Unknown); QStringList signingKeys, encryptionKeys; if (m_cryptoMessageFormat & Kleo::AnyOpenPGP) { if (!m_identity.pgpSigningKey().isEmpty()) { signingKeys.push_back(QLatin1String(m_identity.pgpSigningKey())); } if (!m_identity.pgpEncryptionKey().isEmpty()) { encryptionKeys.push_back(QLatin1String(m_identity.pgpEncryptionKey())); } } if (m_cryptoMessageFormat & Kleo::AnySMIME) { if (!m_identity.smimeSigningKey().isEmpty()) { signingKeys.push_back(QLatin1String(m_identity.smimeSigningKey())); } if (!m_identity.smimeEncryptionKey().isEmpty()) { encryptionKeys.push_back(QLatin1String(m_identity.smimeEncryptionKey())); } } keyResolverCore->setSender(m_identity.fullEmailAddr()); const auto normalized = GpgME::UserID::addrSpecFromString(m_identity.fullEmailAddr().toUtf8().constData()); const auto normalizedSender = QString::fromUtf8(normalized.c_str()); keyResolverCore->setSigningKeys(signingKeys); keyResolverCore->setOverrideKeys({{GpgME::UnknownProtocol, {{normalizedSender, encryptionKeys}}}}); if (encryptSomething) { QStringList recipients; const auto lst = m_recipientsEditor->lines(); for (auto line : lst) { auto recipient = line->data().dynamicCast(); recipients.push_back(recipient->email()); } keyResolverCore->setRecipients(recipients); } return keyResolverCore; } void ComposerViewBase::generateCryptoMessages() { bool canceled = false; qCDebug(EDITOR_LOG) << "filling crypto info"; connect(expiryChecker().get(), &Kleo::ExpiryChecker::expiryMessage, this, [&canceled](const GpgME::Key &key, QString msg, Kleo::ExpiryChecker::ExpiryInformation info, bool isNewMessage) { if (!isNewMessage) { return; } if (canceled) { return; } QString title; QString dontAskAgainName; if (info == Kleo::ExpiryChecker::OwnKeyExpired || info == Kleo::ExpiryChecker::OwnKeyNearExpiry) { dontAskAgainName = QStringLiteral("own key expires soon warning"); } else { dontAskAgainName = QStringLiteral("other encryption key near expiry warning"); } if (info == Kleo::ExpiryChecker::OwnKeyExpired || info == Kleo::ExpiryChecker::OtherKeyExpired) { title = key.protocol() == GpgME::OpenPGP ? i18n("OpenPGP Key Expired") : i18n("S/MIME Certificate Expired"); } else { title = key.protocol() == GpgME::OpenPGP ? i18n("OpenPGP Key Expires Soon") : i18n("S/MIME Certificate Expires Soon"); } if (KMessageBox::warningContinueCancel(nullptr, msg, title, KStandardGuiItem::cont(), KStandardGuiItem::cancel(), dontAskAgainName) == KMessageBox::Cancel) { canceled = true; } }); bool signSomething = m_sign; bool doSignCompletely = m_sign; bool encryptSomething = m_encrypt; bool doEncryptCompletely = m_encrypt; if (m_attachmentModel) { const auto attachments = m_attachmentModel->attachments(); for (const MessageCore::AttachmentPart::Ptr &attachment : attachments) { if (attachment->isSigned()) { signSomething = true; } else { doEncryptCompletely = false; } if (attachment->isEncrypted()) { encryptSomething = true; } else { doSignCompletely = false; } } } qCInfo(EDITOR_LOG) << "Encrypting completely:" << doEncryptCompletely; qCInfo(EDITOR_LOG) << "Signing completely:" << doSignCompletely; // No encryption or signing is needed if (!signSomething && !encryptSomething) { m_composers = { new MessageComposer::Composer }; Q_EMIT composerCreated(); return; } auto keyResolver = fillKeyResolver(encryptSomething); keyResolver->start(true); connect(keyResolver, &Kleo::KeyResolver::keysResolved, this, [this, encryptSomething, signSomething, keyResolver](bool success, bool sendUnencrypted) { if (!success) { qCDebug(EDITOR_LOG) << "resolveAllKeys: failed to resolve keys! oh noes"; Q_EMIT failed(i18n("Failed to resolve keys. Please report a bug.")); return; } qCDebug(EDITOR_LOG) << "done resolving keys."; if (sendUnencrypted) { m_composers = { new MessageComposer::Composer }; Q_EMIT composerCreated(); return; } const auto result = keyResolver->result(); QList composers; auto signingKeyFinder = [&result](const GpgME::Protocol protocol) -> std::optional { for (const auto &key : result.signingKeys) { if (key.protocol() == protocol) { return key; } } return std::nullopt; }; if (encryptSomething || signSomething) { QMap> pgpEncryptionKeys; QMap> smimeEncryptionKeys; if (encryptSomething) { std::vector pgpKeys; QStringList pgpRecipients; std::vector smimeKeys; QStringList smimeRecipients; for (const auto &[recipient, keys] : result.encryptionKeys.asKeyValueRange()) { const auto recipientKeys = result.encryptionKeys[recipient]; if (recipientKeys.size() > 1) { // TODO Carl group handling } else { const auto &key = recipientKeys[0]; if (key.protocol() == GpgME::CMS) { smimeRecipients.append(recipient); smimeKeys.push_back(recipientKeys[0]); } else { pgpRecipients.append(recipient); pgpKeys.push_back(recipientKeys[0]); } } } Q_ASSERT(smimeRecipients.count() == (int)smimeKeys.size()); Q_ASSERT(pgpRecipients.count() == (int)pgpKeys.size()); if (pgpRecipients.count() > 0) { auto composer = new MessageComposer::Composer; composer->setEncryptionKeys({ QPair>(pgpRecipients, pgpKeys) }); auto pgpSigningKey = signingKeyFinder(GpgME::OpenPGP); if (signSomething && pgpSigningKey) { composer->setSigningKeys({ *pgpSigningKey }); } composer->setMessageCryptoFormat(Kleo::OpenPGPMIMEFormat); composer->setSignAndEncrypt(signSomething, encryptSomething); composers << composer; } if (smimeRecipients.count() > 0) { auto composer = new MessageComposer::Composer; composer->setEncryptionKeys({ QPair>(smimeRecipients, smimeKeys) }); auto smimeSigningKey = signingKeyFinder(GpgME::CMS); if (signSomething && smimeSigningKey) { composer->setSigningKeys({ *smimeSigningKey }); } composer->setMessageCryptoFormat(Kleo::SMIMEFormat); composer->setSignAndEncrypt(signSomething, encryptSomething); composers << composer; } } else { // signing only Q_ASSERT(signSomething); Q_ASSERT(!encryptSomething); auto composer = new MessageComposer::Composer; composer->setSignAndEncrypt(signSomething, encryptSomething); Q_ASSERT(result.protocol != GpgME::UnknownProtocol); // No mixed protocol allowed here const auto signingKey = signingKeyFinder(result.protocol); Q_ASSERT(signingKey); composer->setSigningKeys({ *signingKey }); qDebug() << result.protocol; composer->setMessageCryptoFormat(result.protocol == GpgME::OpenPGP ? Kleo::OpenPGPMIMEFormat : Kleo::SMIMEFormat); composers << composer; } } else { auto composer = new MessageComposer::Composer; composers.append(composer); // If we canceled sign or encrypt be sure to change status in attachment. markAllAttachmentsForSigning(false); markAllAttachmentsForEncryption(false); } if (composers.isEmpty() && (signSomething || encryptSomething)) { Q_ASSERT_X(false, "ComposerViewBase::generateCryptoMessages", "No concrete sign or encrypt method selected"); } m_composers = composers; Q_EMIT composerCreated(); }); } void ComposerViewBase::fillGlobalPart(MessageComposer::GlobalPart *globalPart) { globalPart->setParentWidgetForGui(m_parentWidget); globalPart->setCharsets(m_charsets); globalPart->setMDNRequested(m_mdnRequested); globalPart->setRequestDeleveryConfirmation(m_requestDeleveryConfirmation); } void ComposerViewBase::fillInfoPart(MessageComposer::InfoPart *infoPart, ComposerViewBase::RecipientExpansion expansion) { // TODO splitAddressList and expandAliases ugliness should be handled by a // special AddressListEdit widget... (later: see RecipientsEditor) if (expansion == UseExpandedRecipients) { infoPart->setFrom(mExpandedFrom); infoPart->setTo(mExpandedTo); infoPart->setCc(mExpandedCc); infoPart->setBcc(mExpandedBcc); infoPart->setReplyTo(mExpandedReplyTo); } else { infoPart->setFrom(from()); infoPart->setTo(m_recipientsEditor->recipientStringList(Recipient::To)); infoPart->setCc(m_recipientsEditor->recipientStringList(Recipient::Cc)); infoPart->setBcc(m_recipientsEditor->recipientStringList(Recipient::Bcc)); infoPart->setReplyTo(m_recipientsEditor->recipientStringList(Recipient::ReplyTo)); } infoPart->setSubject(subject()); infoPart->setUserAgent(QStringLiteral("KMail")); infoPart->setUrgent(m_urgent); if (auto inReplyTo = m_msg->inReplyTo(false)) { infoPart->setInReplyTo(inReplyTo->asUnicodeString()); } if (auto references = m_msg->references(false)) { infoPart->setReferences(references->asUnicodeString()); } KMime::Headers::Base::List extras; if (auto hdr = m_msg->headerByType("X-KMail-SignatureActionEnabled")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-EncryptActionEnabled")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-CryptoMessageFormat")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-UnExpanded-To")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-UnExpanded-CC")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-UnExpanded-BCC")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-UnExpanded-Reply-To")) { extras << hdr; } if (auto hdr = m_msg->organization(false)) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-Identity")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-Transport")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-Fcc")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-Drafts")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-Templates")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-Link-Message")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-Link-Type")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-Face")) { extras << hdr; } if (auto hdr = m_msg->headerByType("Face")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-FccDisabled")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-Identity-Name")) { extras << hdr; } if (auto hdr = m_msg->headerByType("X-KMail-Transport-Name")) { extras << hdr; } infoPart->setExtraHeaders(extras); } void ComposerViewBase::slotSendComposeResult(KJob *job) { Q_ASSERT(dynamic_cast(job)); auto composer = static_cast(job); if (composer->error() != MessageComposer::Composer::NoError) { qCDebug(EDITOR_LOG) << "compose job might have error: " << job->error() << " errorString: " << job->errorString(); } if (composer->error() == MessageComposer::Composer::NoError) { Q_ASSERT(m_composers.contains(composer)); // The messages were composed successfully. qCDebug(EDITOR_LOG) << "NoError."; const int numberOfMessage(composer->resultMessages().size()); for (int i = 0; i < numberOfMessage; ++i) { queueMessage(composer->resultMessages().at(i)); } } else if (composer->error() == MessageComposer::Composer::UserCancelledError) { // The job warned the user about something, and the user chose to return // to the message. Nothing to do. qCDebug(EDITOR_LOG) << "UserCancelledError."; Q_EMIT failed(i18n("Job cancelled by the user")); } else { qCDebug(EDITOR_LOG) << "other Error." << composer->error(); QString msg; if (composer->error() == MessageComposer::Composer::BugError) { msg = i18n("Could not compose message: %1 \n Please report this bug.", job->errorString()); } else { msg = i18n("Could not compose message: %1", job->errorString()); } Q_EMIT failed(msg); } if (!composer->gnupgHome().isEmpty()) { QDir dir(composer->gnupgHome()); dir.removeRecursively(); } m_composers.removeAll(composer); } void ComposerViewBase::setBearerToken(const QByteArray &bearerToken) { m_bearerToken = bearerToken; } void ComposerViewBase::queueMessage(const KMime::Message::Ptr &message) { qWarning().noquote() << message->encodedContent(); auto soapRequestBody = EwsMailFactory::create(message); QNetworkRequest sendMailRequest(QUrl(u"https://127.0.0.1:5656/socket-web"_s)); sendMailRequest.setHeader(QNetworkRequest::ContentTypeHeader, u"application/xml"_s); sendMailRequest.setRawHeader("X-TOKEN", m_bearerToken); sendMailRequest.setRawHeader("X-EMAIL", from().toUtf8()); const QJsonDocument payload(QJsonObject{ { "type"_L1, "ews"_L1 }, { "payload"_L1, soapRequestBody }, { "id"_L1, mailId() }, }); auto sendMailResponse = qnam->post(sendMailRequest, payload.toJson()); // TODO remove me QObject::connect(qnam, &QNetworkAccessManager::sslErrors, qnam, [](QNetworkReply *reply, const QList &errors) { Q_UNUSED(errors); reply->ignoreSslErrors(); }); - connect(sendMailResponse, &QNetworkReply::finished, this, [sendMailResponse]() { + connect(sendMailResponse, &QNetworkReply::finished, this, [this, sendMailResponse]() { qDebug() << sendMailResponse << sendMailResponse->error() << sendMailResponse->errorString(); + if (sendMailResponse->error() != QNetworkReply::NoError) { + failed(i18nc("Error message", "There were a problem sending the message: %1", sendMailResponse->errorString())); + return; + } + + Q_EMIT sentSuccessfully(); }); qCDebug(EDITOR_LOG) << "Request body" << soapRequestBody; } void ComposerViewBase::initAutoSave() { qCDebug(EDITOR_LOG) << "initialising autosave"; // Ensure that the autosave directory exists. - QDir dataDirectory(DraftManager::draftDirectory()); + QDir dataDirectory(DraftManager::autosaveDirectory()); 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); - } + auto _ = mailId(); // Generate mail id 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; + if (!mailIdEmpty()) { + qCDebug(EDITOR_LOG) << "deleting autosave files" << mailId(); // Delete the autosave files - QDir autoSaveDir(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/kmail2/autosave")); + QDir autoSaveDir(DraftManager::autosaveDirectory()); // Filter out only this composer window's autosave files - const QStringList autoSaveFilter{m_autoSaveUUID + QLatin1String("*")}; + const QStringList autoSaveFilter{mailId() + 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::slotSaveDraft() +{ + qCDebug(EDITOR_LOG) << "Saving draft"; + + if (!m_composers.isEmpty()) { + KMessageBox::error(m_parentWidget, i18n("Could not save the draft.")); + return; + } + + auto composer = new Composer(); + fillComposer(composer); + composer->setDraft(true); + m_composers.append(composer); + connect(composer, &MessageComposer::Composer::result, this, &ComposerViewBase::slotSaveDraftComposeResult); + 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) +void ComposerViewBase::slotSaveDraftComposeResult(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."; + writeDraftToDisk(composer->resultMessages().constFirst()); + Q_ASSERT(composer->resultMessages().size() == 1); + + Q_EMIT sentSuccessfully(); + } 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::writeDraftToDisk(const KMime::Message::Ptr &message) { QDir().mkpath(DraftManager::draftDirectory()); - const QString filename = DraftManager::draftDirectory() + m_autoSaveUUID; + + const QString filename = DraftManager::draftDirectory() + mailId(); + 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) << "Saving draft failed:" << errorMessage << file.errorString() << "mailId" << mailId(); + if (!m_autoSaveErrorShown) { + KMessageBox::error(m_parentWidget, + i18n("Saving the draft 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::writeAutoSaveToDisk(const KMime::Message::Ptr &message) +{ + QDir().mkpath(DraftManager::autosaveDirectory()); + const QString filename = DraftManager::autosaveDirectory() + mailId(); QSaveFile file(filename); QString errorMessage; qCDebug(EDITOR_LOG) << "Writing message to disk as" << filename; if (file.open(QIODevice::WriteOnly)) { file.setPermissions(QFile::ReadUser | QFile::WriteUser); if (file.write(message->encodedContent()) != static_cast(message->encodedContent().size())) { errorMessage = i18n("Could not write all data to file."); } else { if (!file.commit()) { errorMessage = i18n("Could not finalize the file."); } } } else { errorMessage = i18n("Could not open file."); } if (!errorMessage.isEmpty()) { qCWarning(EDITOR_LOG) << "Auto saving failed:" << errorMessage << file.errorString() << " m_autoSaveUUID" << m_autoSaveUUID; if (!m_autoSaveErrorShown) { KMessageBox::error(m_parentWidget, i18n("Autosaving the message as %1 failed.\n" "%2\n" "Reason: %3", filename, errorMessage, file.errorString()), i18nc("@title:window", "Autosaving Message Failed")); // Error dialog shown, hide the errors the next time m_autoSaveErrorShown = true; } } else { // No error occurred, the next error should be shown again m_autoSaveErrorShown = false; } file.commit(); message->clear(); } void ComposerViewBase::addAttachment(const QUrl &url, const QString &comment, bool sync) { Q_UNUSED(comment) qCDebug(EDITOR_LOG) << "adding attachment with url:" << url; if (sync) { m_attachmentController->addAttachmentUrlSync(url); } else { m_attachmentController->addAttachment(url); } } void ComposerViewBase::addAttachment(const QString &name, const QString &filename, const QString &charset, const QByteArray &data, const QByteArray &mimeType) { MessageCore::AttachmentPart::Ptr attachment = MessageCore::AttachmentPart::Ptr(new MessageCore::AttachmentPart()); if (!data.isEmpty()) { attachment->setName(name); attachment->setFileName(filename); attachment->setData(data); attachment->setCharset(charset.toLatin1()); attachment->setMimeType(mimeType); // TODO what about the other fields? m_attachmentController->addAttachment(attachment); } } void ComposerViewBase::addAttachmentPart(KMime::Content *partToAttach) { MessageCore::AttachmentPart::Ptr part(new MessageCore::AttachmentPart); if (partToAttach->contentType()->mimeType() == "multipart/digest" || partToAttach->contentType(false)->mimeType() == "message/rfc822") { // if it is a digest or a full message, use the encodedContent() of the attachment, // which already has the proper headers part->setData(partToAttach->encodedContent()); } else { part->setData(partToAttach->decodedContent()); } part->setMimeType(partToAttach->contentType(false)->mimeType()); if (auto cd = partToAttach->contentDescription(false)) { part->setDescription(cd->asUnicodeString()); } if (auto ct = partToAttach->contentType(false)) { if (ct->hasParameter(QStringLiteral("name"))) { part->setName(ct->parameter(QStringLiteral("name"))); } } if (auto cd = partToAttach->contentDisposition(false)) { part->setFileName(cd->filename()); part->setInline(cd->disposition() == KMime::Headers::CDinline); } if (part->name().isEmpty() && !part->fileName().isEmpty()) { part->setName(part->fileName()); } if (part->fileName().isEmpty() && !part->name().isEmpty()) { part->setFileName(part->name()); } m_attachmentController->addAttachment(part); } void ComposerViewBase::fillComposer(MessageComposer::Composer *composer) { fillComposer(composer, UseUnExpandedRecipients, false); } void ComposerViewBase::fillComposer(MessageComposer::Composer *composer, ComposerViewBase::RecipientExpansion expansion, bool autoresize) { fillInfoPart(composer->infoPart(), expansion); fillGlobalPart(composer->globalPart()); m_editor->fillComposerTextPart(composer->textPart()); fillInfoPart(composer->infoPart(), expansion); if (m_attachmentModel) { composer->addAttachmentParts(m_attachmentModel->attachments(), autoresize); } else { qDebug() << "fillComposer" << "no model"; } } //----------------------------------------------------------------------------- QString ComposerViewBase::to() const { if (m_recipientsEditor) { return MessageComposer::Util::cleanedUpHeaderString(m_recipientsEditor->recipientString(Recipient::To)); } return {}; } //----------------------------------------------------------------------------- QString ComposerViewBase::cc() const { if (m_recipientsEditor) { return MessageComposer::Util::cleanedUpHeaderString(m_recipientsEditor->recipientString(Recipient::Cc)); } return {}; } //----------------------------------------------------------------------------- QString ComposerViewBase::bcc() const { if (m_recipientsEditor) { return MessageComposer::Util::cleanedUpHeaderString(m_recipientsEditor->recipientString(Recipient::Bcc)); } return {}; } QString ComposerViewBase::from() const { return MessageComposer::Util::cleanedUpHeaderString(m_from); } QString ComposerViewBase::replyTo() const { if (m_recipientsEditor) { return MessageComposer::Util::cleanedUpHeaderString(m_recipientsEditor->recipientString(Recipient::ReplyTo)); } return {}; } QString ComposerViewBase::subject() const { return MessageComposer::Util::cleanedUpHeaderString(m_subject); } void ComposerViewBase::setIdentity(const KIdentityManagementCore::Identity &identity) { m_identity = identity; } KIdentityManagementCore::Identity ComposerViewBase::identity() const { return m_identity; } void ComposerViewBase::setParentWidgetForGui(QWidget *w) { m_parentWidget = w; } void ComposerViewBase::setAttachmentController(MessageComposer::AttachmentControllerBase *controller) { m_attachmentController = controller; } MessageComposer::AttachmentControllerBase *ComposerViewBase::attachmentController() { return m_attachmentController; } void ComposerViewBase::setAttachmentModel(MessageComposer::AttachmentModel *model) { m_attachmentModel = model; } MessageComposer::AttachmentModel *ComposerViewBase::attachmentModel() { return m_attachmentModel; } void ComposerViewBase::setRecipientsEditor(RecipientsEditor *recEditor) { m_recipientsEditor = recEditor; } RecipientsEditor *ComposerViewBase::recipientsEditor() { return m_recipientsEditor; } void ComposerViewBase::setSignatureController(MessageComposer::SignatureController *sigController) { m_signatureController = sigController; } MessageComposer::SignatureController *ComposerViewBase::signatureController() { return m_signatureController; } void ComposerViewBase::updateRecipients(const KIdentityManagementCore::Identity &ident, const KIdentityManagementCore::Identity &oldIdent, Recipient::Type type) { QString oldIdentList; QString newIdentList; if (type == Recipient::Bcc) { oldIdentList = oldIdent.bcc(); newIdentList = ident.bcc(); } else if (type == Recipient::Cc) { oldIdentList = oldIdent.cc(); newIdentList = ident.cc(); } else if (type == Recipient::ReplyTo) { oldIdentList = oldIdent.replyToAddr(); newIdentList = ident.replyToAddr(); } else { return; } if (oldIdentList != newIdentList) { const auto oldRecipients = KMime::Types::Mailbox::listFromUnicodeString(oldIdentList); for (const KMime::Types::Mailbox &recipient : oldRecipients) { m_recipientsEditor->removeRecipient(recipient.prettyAddress(), type); } const auto newRecipients = KMime::Types::Mailbox::listFromUnicodeString(newIdentList); for (const KMime::Types::Mailbox &recipient : newRecipients) { m_recipientsEditor->addRecipient(recipient.prettyAddress(), type); } m_recipientsEditor->setFocusBottom(); } } void ComposerViewBase::identityChanged(const KIdentityManagementCore::Identity &ident, const KIdentityManagementCore::Identity &oldIdent, bool msgCleared) { updateRecipients(ident, oldIdent, Recipient::Bcc); updateRecipients(ident, oldIdent, Recipient::Cc); updateRecipients(ident, oldIdent, Recipient::ReplyTo); KIdentityManagementCore::Signature oldSig = const_cast(oldIdent).signature(); KIdentityManagementCore::Signature newSig = const_cast(ident).signature(); // replace existing signatures const bool replaced = editor()->composerSignature()->replaceSignature(oldSig, newSig); // Just append the signature if there was no old signature if (!replaced && (msgCleared || oldSig.rawText().isEmpty())) { signatureController()->applySignature(newSig); } m_editor->setAutocorrectionLanguage(ident.autocorrectionLanguage()); } void ComposerViewBase::setEditor(MessageComposer::RichTextComposerNg *editor) { m_editor = editor; m_editor->document()->setModified(false); } MessageComposer::RichTextComposerNg *ComposerViewBase::editor() const { return m_editor; } void ComposerViewBase::setFrom(const QString &from) { m_from = from; } void ComposerViewBase::setSubject(const QString &subject) { m_subject = subject; } void ComposerViewBase::setAutoSaveInterval(int interval) { m_autoSaveInterval = interval; } void ComposerViewBase::setCryptoOptions(bool sign, bool encrypt, Kleo::CryptoMessageFormat format, bool neverEncryptDrafts) { m_sign = sign; m_encrypt = encrypt; m_cryptoMessageFormat = format; m_neverEncrypt = neverEncryptDrafts; } void ComposerViewBase::setCharsets(const QList &charsets) { m_charsets = charsets; } void ComposerViewBase::setMDNRequested(bool mdnRequested) { m_mdnRequested = mdnRequested; } void ComposerViewBase::setUrgent(bool urgent) { m_urgent = urgent; } int ComposerViewBase::autoSaveInterval() const { return m_autoSaveInterval; } //----------------------------------------------------------------------------- void ComposerViewBase::collectImages(KMime::Content *root) { if (KMime::Content *n = Util::findTypeInMessage(root, "multipart", "alternative")) { KMime::Content *parentnode = n->parent(); if (parentnode && parentnode->contentType()->isMultipart() && parentnode->contentType()->subType() == "related") { KMime::Content *node = MessageCore::NodeHelper::nextSibling(n); while (node) { if (node->contentType()->isImage()) { qCDebug(EDITOR_LOG) << "found image in multipart/related : " << node->contentType()->name(); QImage img; img.loadFromData(node->decodedContent()); m_editor->composerControler()->composerImages()->loadImage( img, QString::fromLatin1(QByteArray(QByteArrayLiteral("cid:") + node->contentID()->identifier())), node->contentType()->name()); } node = MessageCore::NodeHelper::nextSibling(node); } } } } //----------------------------------------------------------------------------- bool ComposerViewBase::inlineSigningEncryptionSelected() const { if (!m_sign && !m_encrypt) { return false; } return m_cryptoMessageFormat == Kleo::InlineOpenPGPFormat; } bool ComposerViewBase::hasMissingAttachments(const QStringList &attachmentKeywords) { if (attachmentKeywords.isEmpty()) { return false; } if (m_attachmentModel && m_attachmentModel->rowCount() > 0) { return false; } return MessageComposer::Util::hasMissingAttachments(attachmentKeywords, m_editor->document(), subject()); } ComposerViewBase::MissingAttachment ComposerViewBase::checkForMissingAttachments(const QStringList &attachmentKeywords) { if (!hasMissingAttachments(attachmentKeywords)) { return NoMissingAttachmentFound; } const int rc = KMessageBox::warningTwoActionsCancel(m_editor, i18n("The message you have composed seems to refer to an " "attached file but you have not attached anything.\n" "Do you want to attach a file to your message?"), i18nc("@title:window", "File Attachment Reminder"), KGuiItem(i18n("&Attach File..."), QLatin1String("mail-attachment")), KGuiItem(i18n("&Send as Is"), QLatin1String("mail-send"))); if (rc == KMessageBox::Cancel) { return FoundMissingAttachmentAndCancel; } if (rc == KMessageBox::ButtonCode::PrimaryAction) { m_attachmentController->showAddAttachmentFileDialog(); return FoundMissingAttachmentAndAddedAttachment; } return FoundMissingAttachmentAndSending; } void ComposerViewBase::markAllAttachmentsForSigning(bool sign) { if (m_attachmentModel) { const auto attachments = m_attachmentModel->attachments(); for (MessageCore::AttachmentPart::Ptr attachment : attachments) { attachment->setSigned(sign); } } } void ComposerViewBase::markAllAttachmentsForEncryption(bool encrypt) { if (m_attachmentModel) { const auto attachments = m_attachmentModel->attachments(); for (MessageCore::AttachmentPart::Ptr attachment : attachments) { attachment->setEncrypted(encrypt); } } } bool ComposerViewBase::requestDeleveryConfirmation() const { return m_requestDeleveryConfirmation; } void ComposerViewBase::setRequestDeleveryConfirmation(bool requestDeleveryConfirmation) { m_requestDeleveryConfirmation = requestDeleveryConfirmation; } KMime::Message::Ptr ComposerViewBase::msg() const { return m_msg; } std::shared_ptr ComposerViewBase::expiryChecker() { if (!mExpiryChecker) { mExpiryChecker.reset(new Kleo::ExpiryChecker{Kleo::ExpiryCheckerSettings{encryptOwnKeyNearExpiryWarningThresholdInDays(), encryptKeyNearExpiryWarningThresholdInDays(), encryptRootCertNearExpiryWarningThresholdInDays(), encryptChainCertNearExpiryWarningThresholdInDays()}}); } return mExpiryChecker; } QString ComposerViewBase::mailId() const { if (m_autoSaveUUID.isEmpty()) { m_autoSaveUUID = QUuid::createUuid().toString(QUuid::WithoutBraces); } return m_autoSaveUUID; } +bool ComposerViewBase::mailIdEmpty() const +{ + return m_autoSaveUUID.isEmpty(); +} + #include "moc_composerviewbase.cpp" diff --git a/client/editor/composerviewbase.h b/client/editor/composerviewbase.h index 6985b43..bc51260 100644 --- a/client/editor/composerviewbase.h +++ b/client/editor/composerviewbase.h @@ -1,354 +1,369 @@ /* 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 "identity/identity.h" 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. */ 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 whether the mail id is empty + [[nodiscard]] bool mailIdEmpty() 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. - */ + /// Save the message in the autosave folder. + /// + /// This message will be reopened automatically in the next section if + /// it wasn't cleaned up with cleanupAutoSave void autoSaveMessage(); + /// Save the message as a draft. + void slotSaveDraft(); + /// 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 slotSaveDraftComposeResult(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); 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); + /** + * Writes out draft 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 writeDraftToDisk(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/client/editor/composerwindow.cpp b/client/editor/composerwindow.cpp index 23f65f1..6a2cc7b 100644 --- a/client/editor/composerwindow.cpp +++ b/client/editor/composerwindow.cpp @@ -1,1715 +1,1759 @@ // 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 "identity/identity.h" #include #include #include #include #include #include // Gpgme includes #include #include // App includes #include "../identity/identitymanager.h" #include "../identity/identitydialog.h" #include "recipientseditor.h" #include "nearexpirywarning.h" #include "composerviewbase.h" #include "richtextcomposerng.h" #include "signaturecontroller.h" #include "job/saveasfilejob.h" #include "job/inserttextfilejob.h" #include "attachment/attachmentcontroller.h" #include "attachment/attachmentview.h" #include "attachment/attachmentmodel.h" #include "kmcomposerglobalaction.h" #include "mailtemplates.h" #include "messagecomposersettings.h" #include "spellcheckerconfigdialog.h" #include "websocketclient.h" #include "draft/draftmanager.h" using namespace Qt::Literals::StringLiterals; using namespace std::chrono_literals; namespace { inline bool containsSMIME(unsigned int f) { return f & (Kleo::SMIMEFormat | Kleo::SMIMEOpaqueFormat); } inline bool containsOpenPGP(unsigned int f) { return f & (Kleo::OpenPGPMIMEFormat | Kleo::InlineOpenPGPFormat); } auto findSendersUid(const std::string &addrSpec, const std::vector &userIds) { return std::find_if(userIds.cbegin(), userIds.cend(), [&addrSpec](const auto &uid) { return uid.addrSpec() == addrSpec || (uid.addrSpec().empty() && std::string(uid.email()) == addrSpec) || (uid.addrSpec().empty() && (!uid.email() || !*uid.email()) && uid.name() == addrSpec); }); } } ComposerWindow::ComposerWindow(const QString &from, const QString &name, const QByteArray &bearerToken, QWidget *parent) : KXmlGuiWindow(parent) , mFrom(from) , mMainWidget(new QWidget(this)) , mComposerBase(new MessageComposer::ComposerViewBase(this)) , mHeadersToEditorSplitter(new QSplitter(Qt::Vertical, mMainWidget)) , mHeadersArea(new QWidget(mHeadersToEditorSplitter)) , mGrid(new QGridLayout(mHeadersArea)) , mLblFrom(new QLabel(i18nc("sender address field", "From:"), mHeadersArea)) , mButtonFrom(new QPushButton(mHeadersArea)) , mRecipientEditor(new RecipientsEditor(mHeadersArea)) , mLblSubject(new QLabel(i18nc("@label:textbox Subject of email.", "Subject:"), mHeadersArea)) , mEdtSubject(new QLineEdit(mHeadersArea)) , mRichTextComposer(new MessageComposer::RichTextComposerNg(this)) , mRichTextEditorWidget(new TextCustomEditor::RichTextEditorWidget(mRichTextComposer, mMainWidget)) , mNearExpiryWarning(new NearExpiryWarning(this)) , mGlobalAction(new KMComposerGlobalAction(this, this)) , mKeyCache(Kleo::KeyCache::mutableInstance()) { connect(mKeyCache.get(), &Kleo::KeyCache::keyListingDone, this, [this](const GpgME::KeyListResult &result) { Q_UNUSED(result); mRunKeyResolverTimer->start(); }); bool isNew = false; mIdentity = IdentityManager::self().fromEmail(from, isNew); mEdtFrom = new QLabel(mHeadersArea); if (isNew) { // fill the idenity with default fields auto dlg = new KMail::IdentityDialog; mIdentity.setFullName(name); mIdentity.setWarnNotEncrypt(true); dlg->setIdentity(mIdentity); connect(dlg, &KMail::IdentityDialog::keyListingFinished, this, [this, dlg]() { dlg->updateIdentity(mIdentity); IdentityManager::self().updateIdentity(mIdentity); }); } mComposerBase->setBearerToken(bearerToken); mMainWidget->resize(800, 600); setCentralWidget(mMainWidget); setWindowTitle(i18nc("@title:window", "Composer")); setMinimumSize(200, 200); mHeadersToEditorSplitter->setObjectName(QStringLiteral("mHeadersToEditorSplitter")); mHeadersToEditorSplitter->setChildrenCollapsible(false); auto v = new QVBoxLayout(mMainWidget); v->setContentsMargins({}); v->addWidget(mNearExpiryWarning); v->addWidget(mHeadersToEditorSplitter); mHeadersArea->setSizePolicy(mHeadersToEditorSplitter->sizePolicy().horizontalPolicy(), QSizePolicy::Expanding); mHeadersToEditorSplitter->addWidget(mHeadersArea); const QList defaultSizes{0}; mHeadersToEditorSplitter->setSizes(defaultSizes); mGrid->setColumnStretch(0, 1); mGrid->setColumnStretch(1, 100); mGrid->setRowStretch(3 + 1, 100); int row = 0; mRunKeyResolverTimer = new QTimer(this); mRunKeyResolverTimer->setSingleShot(true); mRunKeyResolverTimer->setInterval(500ms); connect(mRunKeyResolverTimer, &QTimer::timeout, this, &ComposerWindow::runKeyResolver); // From mLblFrom->setObjectName(QStringLiteral("fromLineLabel")); mLblFrom->setFixedWidth(mRecipientEditor->setFirstColumnWidth(0)); mLblFrom->setBuddy(mEdtFrom); auto fromWrapper = new QWidget(mHeadersArea); auto fromWrapperLayout = new QHBoxLayout(fromWrapper); fromWrapperLayout->setContentsMargins({}); mEdtFrom->installEventFilter(this); mEdtFrom->setText(mFrom); mEdtFrom->setObjectName(QStringLiteral("fromLine")); fromWrapperLayout->addWidget(mEdtFrom); mComposerBase->setIdentity(mIdentity); mButtonFrom->setText(i18nc("@action:button", "Configure")); mButtonFrom->setIcon(QIcon::fromTheme(u"configure-symbolic"_s)); connect(mButtonFrom, &QPushButton::clicked, this, &ComposerWindow::slotEditIdentity); fromWrapperLayout->addWidget(mButtonFrom); mGrid->addWidget(mLblFrom, row, 0); mGrid->addWidget(fromWrapper, row, 1); row++; // Recipients mGrid->addWidget(mRecipientEditor, row, 0, 1, 2); mComposerBase->setRecipientsEditor(mRecipientEditor); mRecipientEditor->setCompletionMode(KCompletion::CompletionPopup); connect(mRecipientEditor, &RecipientsEditor::lineAdded, this, [this](KPIM::MultiplyingLine *line) { slotRecipientEditorLineAdded(qobject_cast(line)); }); row++; // Subject mEdtSubject->setObjectName(u"subjectLine"_s); mLblSubject->setObjectName(u"subjectLineLabel"_s); mLblSubject->setBuddy(mEdtSubject); mGrid->addWidget(mLblSubject, row, 0); mGrid->addWidget(mEdtSubject, row, 1); row++; auto editorWidget = new QWidget(); auto vLayout = new QVBoxLayout(editorWidget); vLayout->setContentsMargins({}); vLayout->setSpacing(0); mHeadersToEditorSplitter->addWidget(editorWidget); // Message widget auto connectionLossWidget = new KMessageWidget(this); connectionLossWidget->hide(); connectionLossWidget->setWordWrap(true); connectionLossWidget->setPosition(KMessageWidget::Position::Header); vLayout->addWidget(connectionLossWidget); auto &websocketClient = WebsocketClient::self(); connect(&websocketClient, &WebsocketClient::closed, this, [connectionLossWidget](const QString &errorMessage) { connectionLossWidget->setText(errorMessage); connectionLossWidget->show(); }); connect(&websocketClient, &WebsocketClient::connected, this, [connectionLossWidget]() { connectionLossWidget->hide(); }); connect(&websocketClient, &WebsocketClient::emailSentSuccessfully, this, [this](const QString &id) { if (id == mComposerBase->mailId()) { auto &draftManager = DraftManager::self(); draftManager.remove(draftManager.draftById(id.toUtf8())); hide(); } }); // Rich text editor mRichTextComposer->setProperty("_breeze_borders_sides", QVariant::fromValue(QFlags{Qt::TopEdge})); mRichTextComposer->setProperty("_breeze_force_frame", true); mComposerBase->setEditor(mRichTextComposer); vLayout->addWidget(mRichTextEditorWidget); auto attachmentModel = new MessageComposer::AttachmentModel(this); auto attachmentView = new AttachmentView(attachmentModel, mHeadersToEditorSplitter); attachmentView->hideIfEmpty(); connect(attachmentView, &AttachmentView::modified, this, &ComposerWindow::setModified); auto attachmentController = new AttachmentController(attachmentModel, attachmentView, this); mComposerBase->setAttachmentController(attachmentController); mComposerBase->setAttachmentModel(attachmentModel); auto signatureController = new MessageComposer::SignatureController(this); connect(signatureController, &MessageComposer::SignatureController::enableHtml, this, &ComposerWindow::enableHtml); signatureController->setIdentity(mIdentity); signatureController->setEditor(mComposerBase->editor()); mComposerBase->setSignatureController(signatureController); connect(signatureController, &MessageComposer::SignatureController::signatureAdded, mComposerBase->editor()->externalComposer(), &KPIMTextEdit::RichTextExternalComposer::startExternalEditor); setupStatusBar(attachmentView->widget()); setupActions(); setStandardToolBarMenuEnabled(true); toolBar(u"mainToolBar"_s)->show(); connect(expiryChecker().get(), &Kleo::ExpiryChecker::expiryMessage, this, [&](const GpgME::Key &key, QString msg, Kleo::ExpiryChecker::ExpiryInformation info, bool isNewMessage) { Q_UNUSED(isNewMessage); if (info == Kleo::ExpiryChecker::OwnKeyExpired || info == Kleo::ExpiryChecker::OwnKeyNearExpiry) { const auto plainMsg = msg.replace(QStringLiteral("

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

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

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

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

" "

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

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

" "Select the certificate(s) which should " "be used for this recipient. If there is no suitable certificate in the list " "you can also search for external certificates by clicking the button: " "search for external certificates.
", recipient->name().isEmpty() ? recipient->email() : recipient->name()); const bool opgp = containsOpenPGP(cryptoMessageFormat()); const bool x509 = containsSMIME(cryptoMessageFormat()); QPointer dlg = new Kleo::KeySelectionDialog( i18n("Encryption Key Selection"), msg, recipient->email(), {}, Kleo::KeySelectionDialog::ValidEncryptionKeys | (opgp ? Kleo::KeySelectionDialog::OpenPGPKeys : 0) | (x509 ? Kleo::KeySelectionDialog::SMIMEKeys : 0), false, // multi-selection false); // "remember choice" box; dlg->open(); connect(dlg, &QDialog::accepted, this, [dlg, recipient, line, this]() { auto key = dlg->selectedKey(); key.update(); // We need tofu information for key. recipient->setKey(key); annotateRecipientEditorLineWithCryptoInfo(line); }); } void ComposerWindow::slotRecipientEditorFocusChanged() { if (!mEncryptAction->isChecked()) { return; } if (mKeyCache->initialized()) { mRunKeyResolverTimer->stop(); runKeyResolver(); } } void ComposerWindow::slotRecipientAdded(RecipientLineNG *line) { if (line->recipientsCount() == 0) { return; } if (!mKeyCache->initialized()) { if (line->property("keyLookupJob").toBool()) { return; } line->setProperty("keyLookupJob", true); // We need to start key listing on our own othweise KMail will crash and we want to wait till the cache is populated. connect(mKeyCache.get(), &Kleo::KeyCache::keyListingDone, this, [this, line]() { slotRecipientAdded(line); }); return; } if (mKeyCache->initialized()) { mRunKeyResolverTimer->start(); } } void ComposerWindow::slotRecipientFocusLost(RecipientLineNG *line) { if (line->recipientsCount() == 0) { return; } if (mKeyCache->initialized()) { mRunKeyResolverTimer->start(); } } + + +void ComposerWindow::slotSendSuccessful() +{ + setModified(false); + mComposerBase->cleanupAutoSave(); + close(); +} diff --git a/client/editor/composerwindow.h b/client/editor/composerwindow.h index 4b6c3fc..7160141 100644 --- a/client/editor/composerwindow.h +++ b/client/editor/composerwindow.h @@ -1,226 +1,229 @@ // SPDX-FileCopyrightText: 2023 g10 code Gmbh // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #pragma once // Qt includes #include // KDE includes #include #include #include #include #include #include "identity/identity.h" // App includes #include "composerwindowfactory.h" class QSplitter; class QLabel; class QPrinter; class QGridLayout; class QLineEdit; class QPushButton; class KLineEdit; class RecipientsEditor; class KToggleAction; class RecipientLineNG; class NearExpiryWarning; class IdentityCombo; class KMComposerGlobalAction; class KRecentFilesAction; namespace KPIMTextEdit { class RichTextComposerWidget; class RichTextComposer; } namespace TextCustomEditor { class RichTextEditorWidget; } namespace MessageComposer { class ComposerViewBase; class RichTextComposerNg; } namespace Kleo { class KeyResolverCore; class ExpiryChecker; } class ComposerWindow : public KXmlGuiWindow { Q_OBJECT enum Confirmation { LetUserConfirm, NoConfirmationNeeded, }; public: struct AttachmentInfo { QString comment; QUrl url; }; /// The identity assigned to this message. KIdentityManagementCore::Identity identity() const; /// The subject of the message. QString subject() const; /// The recipients of the message. RecipientsEditor *recipientsEditor() const; /// The content of the message. QString content() const; void addAttachment(const QList &infos, bool showWarning); void reply(const KMime::Message::Ptr &message); void forward(const KMime::Message::Ptr &message); void setMessage(const KMime::Message::Ptr &message); + private Q_SLOTS: void slotSend(); void slotToggleMarkup(); void slotSignToggled(bool on); void slotSaveDraft(); void slotSaveAsFile(); void slotInsertFile(); void slotEncryptionButtonIconUpdate(); void slotEditIdentity(); void slotIdentityChanged(); void slotPrint(); void slotPrintPreview(); void slotWordWrapToggled(bool on); void slotAutoSpellCheckingToggled(bool enabled); void slotSpellcheckConfig(); void printInternal(QPrinter *printer); void enableHtml(); void slotPasteAsAttachment(); void disableHtml(Confirmation confirmation); void slotCursorPositionChanged(); void slotInsertRecentFile(const QUrl &u); void slotRecentListFileClear(); void slotRecipientEditorFocusChanged(); void slotRecipientAdded(RecipientLineNG *line); void slotRecipientFocusLost(RecipientLineNG *line); void slotRecipientEditorLineAdded(RecipientLineNG *line); void slotRecipientLineIconClicked(RecipientLineNG *line); + void slotSendSuccessful(); void insertUrls(const QMimeData *source, const QList &urlList); bool insertFromMimeData(const QMimeData *source, bool forceAttachment); QUrl insertFile(); void addAttachment(const QString &name, KMime::Headers::contentEncoding cte, const QString &charset, const QByteArray &data, const QByteArray &mimeType); /// Set whether the message will be signed. void setSigning(bool sign, bool setByUser = false); /// Set whether the message should be treated as modified or not. void setModified(bool modified); std::shared_ptr expiryChecker(); Q_SIGNALS: void identityChanged(); void initialized(); protected: friend ComposerWindowFactory; explicit ComposerWindow(const QString &fromAddress, const QString &name, const QByteArray &bearerToken, QWidget *parent = nullptr); void reset(const QString &fromAddress, const QString &name, const QByteArray &bearerToken); void closeEvent(QCloseEvent *event) override; private: enum CryptoKeyState { NoState = 0, InProgress, KeyOk, NoKey, NoTrusted, }; /// Ask for confirmation if the message was changed. [[nodiscard]] bool queryClose() override; void annotateRecipientEditorLineWithCryptoInfo(RecipientLineNG *line); void setupActions(); void setupStatusBar(QWidget *w); void htmlToolBarVisibilityChanged(bool visible); void changeCryptoAction(); void runKeyResolver(); int validateLineWrapWidth() const; void disableWordWrap(); void checkOwnKeyExpiry(const KIdentityManagementCore::Identity &ident); std::unique_ptr fillKeyResolver(); - /** - * Returns true if the message was modified by the user. - */ + /// Returns true if the message was modified by the user. [[nodiscard]] bool isModified() const; + /// Returns true if the composer content was modified by the user. + [[nodiscard]] bool isComposerModified() const; + /** * Returns true if the message will be signed. */ [[nodiscard]] bool sign() const; Kleo::CryptoMessageFormat cryptoMessageFormat() const; KIdentityManagementCore::Identity mIdentity; QString mFrom; QWidget *const mMainWidget; // splitter between the headers area and the actual editor MessageComposer::ComposerViewBase *const mComposerBase; QSplitter *const mHeadersToEditorSplitter; QWidget *const mHeadersArea; QGridLayout *const mGrid; QLabel *const mLblFrom; QPushButton *const mButtonFrom; RecipientsEditor *const mRecipientEditor; QLabel *const mLblSubject; QLineEdit *const mEdtSubject; MessageComposer::RichTextComposerNg *const mRichTextComposer; TextCustomEditor::RichTextEditorWidget *const mRichTextEditorWidget; NearExpiryWarning *const mNearExpiryWarning; KMComposerGlobalAction *const mGlobalAction; QLabel *mEdtFrom = nullptr; bool mForceDisableHtml = false; bool mLastIdentityHasSigningKey = false; bool mLastIdentityHasEncryptionKey = false; QAction *mEncryptAction = nullptr; QAction *mSignAction = nullptr; QAction *mAppendSignature = nullptr; QAction *mPrependSignature = nullptr; QAction *mInsertSignatureAtCursorPosition = nullptr; QAction *mSelectAll = nullptr; QAction *mFindText = nullptr; QAction *mFindNextText = nullptr; QAction *mReplaceText = nullptr; QLabel *mStatusbarLabel = nullptr; QLabel *mCursorLineLabel = nullptr; QLabel *mCursorColumnLabel = nullptr; KToggleAction *mMarkupAction = nullptr; std::shared_ptr mExpiryChecker; bool mIsModified = false; bool mAcceptedSolution = false; KRecentFilesAction *mRecentAction = nullptr; KToggleAction *mWordWrapAction = nullptr; KToggleAction *mAutoSpellCheckingAction = nullptr; bool mLastSignActionState = false; std::shared_ptr mKeyCache; bool mLastEncryptActionState = false; QTimer *mRunKeyResolverTimer = nullptr; }; diff --git a/client/editor/composerwindowfactory.cpp b/client/editor/composerwindowfactory.cpp index 25e96d4..c843504 100644 --- a/client/editor/composerwindowfactory.cpp +++ b/client/editor/composerwindowfactory.cpp @@ -1,37 +1,42 @@ // SPDX-FileCopyrightText: 2023 g10 code Gmbh // SPDX-Contributor: Carl Schwan // SPDX-License-Identifier: GPL-2.0-or-later #include "composerwindowfactory.h" #include "editor/composerwindow.h" ComposerWindowFactory::ComposerWindowFactory() = default; ComposerWindowFactory &ComposerWindowFactory::self() { static ComposerWindowFactory instance; return instance; } ComposerWindow *ComposerWindowFactory::create(const QString &fromAddress, const QString &name, const QByteArray &bearerToken) { if (inactiveWindow) { auto window = inactiveWindow; inactiveWindow = nullptr; window->reset(fromAddress, name, bearerToken); return window; } return new ComposerWindow(fromAddress, name, bearerToken); } void ComposerWindowFactory::clear(ComposerWindow *composerWindow) { + auto close = composerWindow->queryClose(); + if (!close) { + return; + } + if (inactiveWindow) { composerWindow->deleteLater(); return; } inactiveWindow = composerWindow; inactiveWindow->hide(); }