diff --git a/client/editor/composer.cpp b/client/editor/composer.cpp index 29298b7..7c2fdc2 100644 --- a/client/editor/composer.cpp +++ b/client/editor/composer.cpp @@ -1,686 +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 && !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) +void Composer::setCryptoMessageFormat(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 adeef29..58f6ae4 100644 --- a/client/editor/composer.h +++ b/client/editor/composer.h @@ -1,89 +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 setCryptoMessageFormat(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 556aaeb..128190f 100644 --- a/client/editor/composerviewbase.cpp +++ b/client/editor/composerviewbase.cpp @@ -1,1455 +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->setCryptoMessageFormat(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->setCryptoMessageFormat(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); + composer->setCryptoMessageFormat(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, [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::autosaveDirectory()); if (!dataDirectory.exists(QStringLiteral("autosave"))) { qCDebug(EDITOR_LOG) << "Creating autosave directory."; dataDirectory.mkdir(QStringLiteral("autosave")); } 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 (!mailIdEmpty()) { qCDebug(EDITOR_LOG) << "deleting autosave files" << mailId(); // Delete the autosave files QDir autoSaveDir(DraftManager::autosaveDirectory()); // Filter out only this composer window's autosave files 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::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() + 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"