Page Menu
Home
GnuPG
Search
Configure Global Search
Log In
Files
F31766724
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Size
51 KB
Subscribers
None
View Options
diff --git a/client/editor/composerviewbase.cpp b/client/editor/composerviewbase.cpp
index 2a39a41..feff3d4 100644
--- a/client/editor/composerviewbase.cpp
+++ b/client/editor/composerviewbase.cpp
@@ -1,1386 +1,1410 @@
/*
SPDX-FileCopyrightText: 2010 Klaralvdalens Datakonsult AB, a KDAB Group company, info@kdab.com
SPDX-FileCopyrightText: 2010 Leo Franchi <lfranchi@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "composerviewbase.h"
#include "attachment/attachmentcontrollerbase.h"
#include "attachment/attachmentmodel.h"
#include "job/composerjob.h"
#include "mailtemplates.h"
#include "messagedispatcher.h"
#include "nodehelper.h"
#include "part/globalpart.h"
#include "part/infopart.h"
#include "richtextcomposerng.h"
#include "richtextcomposersignatures.h"
#include "signaturecontroller.h"
#include "util.h"
#include "util_p.h"
#include "messagecomposersettings.h"
#include "recipientseditor.h"
#include "identity/identity.h"
#include <KCursorSaver>
#include <KEmailAddress>
#include <KPIMTextEdit/RichTextComposerControler>
#include <KPIMTextEdit/RichTextComposerImages>
#include <MimeTreeParserCore/ObjectTreeParser>
#include <Sonnet/DictionaryComboBox>
#include "editor_debug.h"
#include <Libkleo/ExpiryChecker>
#include <Libkleo/ExpiryCheckerSettings>
#include <Libkleo/KeyCache>
#include <Libkleo/KeyResolverCore>
#include <libkleo/enum.h>
#include <QGpgME/ExportJob>
#include <QGpgME/ImportJob>
#include <QGpgME/Protocol>
#include <global.h>
#include <gpgme++/context.h>
#include <gpgme++/importresult.h>
#include <KLocalizedString>
#include <KMessageBox>
#include <QDir>
#include <QSaveFile>
#include <QStandardPaths>
#include <QTemporaryDir>
#include <QTimer>
#include <QUuid>
#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
{
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);
}
m_subject = m_msg->subject()->asUnicodeString();
// 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.
KMime::Content msgContent;
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) {
Q_EMIT enableHtml();
editor()->activateRichText();
} else {
editor()->switchToPlainText();
Q_EMIT disableHtml(LetUserConfirm);
}
editor()->setText(body);
if (auto hdr = m_msg->headerByType("X-KMail-CursorPos")) {
m_editor->setCursorPositionFromStart(hdr->asUnicodeString().toUInt());
}
}
void ComposerViewBase::saveMailSettings()
{
auto header = new KMime::Headers::Generic("X-KMail-Identity");
header->fromUnicodeString(QString::number(m_identity.uoid()));
m_msg->setHeader(header);
header = new KMime::Headers::Generic("X-KMail-Identity-Name");
header->fromUnicodeString(m_identity.identityName());
m_msg->setHeader(header);
header = new KMime::Headers::Generic("X-KMail-Dictionary");
header->fromUnicodeString(m_dictionary->currentDictionary());
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());
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"));
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("<qt><p>Inline signing/encrypting of HTML messages is not possible;</p>"
"<p>do you want to delete your markup?</p></qt>"),
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<QByteArray, QString> &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::ComposerJob;
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::ComposerJob *composer : composers) {
fillComposer(composer, UseExpandedRecipients, false);
connect(composer, &MessageComposer::ComposerJob::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)};
}
} // nameless namespace
Kleo::KeyResolver *ComposerViewBase::fillKeyResolver(bool encryptSomething)
{
auto keyResolverCore = new Kleo::KeyResolver(m_encrypt, m_sign, GpgME::UnknownProtocol, false);
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<Recipient>();
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::ComposerJob};
Q_EMIT composerCreated();
return;
}
auto keyResolver = fillKeyResolver(encryptSomething);
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::ComposerJob};
Q_EMIT composerCreated();
return;
}
const auto result = keyResolver->result();
QList<MessageComposer::ComposerJob *> composers;
auto signingKeyFinder = [&result](const GpgME::Protocol protocol) -> std::optional<GpgME::Key> { // clazy:excludeall=lambda-in-connect
for (const auto &key : result.signingKeys) {
if (key.protocol() == protocol) {
return key;
}
}
return std::nullopt;
};
if (encryptSomething || signSomething) {
if (encryptSomething) {
std::vector<GpgME::Key> pgpKeys;
QStringList pgpRecipients;
std::vector<GpgME::Key> 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::ComposerJob;
composer->setEncryptionKeys({QPair<QStringList, std::vector<GpgME::Key>>(pgpRecipients, pgpKeys)});
auto pgpSigningKey = signingKeyFinder(GpgME::OpenPGP);
if (signSomething && pgpSigningKey) {
composer->setSigningKeys({*pgpSigningKey});
}
composer->setCryptoMessageFormat(Kleo::OpenPGPMIMEFormat);
composer->setSignAndEncrypt(signSomething, encryptSomething);
composers << composer;
}
if (smimeRecipients.count() > 0) {
auto composer = new MessageComposer::ComposerJob;
composer->setEncryptionKeys({QPair<QStringList, std::vector<GpgME::Key>>(smimeRecipients, smimeKeys)});
auto smimeSigningKey = signingKeyFinder(GpgME::CMS);
if (signSomething && smimeSigningKey) {
composer->setSigningKeys({*smimeSigningKey});
}
composer->setCryptoMessageFormat(Kleo::SMIMEFormat);
composer->setSignAndEncrypt(signSomething, encryptSomething);
composers << composer;
}
} else {
// signing only
Q_ASSERT(signSomething);
Q_ASSERT(!encryptSomething);
auto composer = new MessageComposer::ComposerJob;
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});
composer->setCryptoMessageFormat(result.protocol == GpgME::OpenPGP ? Kleo::OpenPGPMIMEFormat : Kleo::SMIMEFormat);
composers << composer;
}
} else {
auto composer = new MessageComposer::ComposerJob;
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();
qWarning() << "composer created" << composers;
});
keyResolver->start(false, m_parentWidget);
}
void ComposerViewBase::fillGlobalPart(MessageComposer::GlobalPart *globalPart)
{
globalPart->setParentWidgetForGui(m_parentWidget);
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-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;
}
infoPart->setExtraHeaders(extras);
}
void ComposerViewBase::slotSendComposeResult(KJob *job)
{
Q_ASSERT(dynamic_cast<MessageComposer::ComposerJob *>(job));
auto composer = static_cast<MessageComposer::ComposerJob *>(job);
if (composer->error() != MessageComposer::ComposerJob::NoError) {
qCDebug(EDITOR_LOG) << "compose job might have error: " << job->error() << " errorString: " << job->errorString();
}
if (composer->error() == MessageComposer::ComposerJob::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::ComposerJob::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::ComposerJob::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::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"));
}
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);
connect(m_autoSaveTimer, &QTimer::timeout, this, &ComposerViewBase::autoSaveMessage);
}
m_autoSaveTimer->start(m_autoSaveInterval);
}
}
void ComposerViewBase::cleanupAutoSave()
{
delete m_autoSaveTimer;
m_autoSaveTimer = nullptr;
if (!mailIdIsEmpty()) {
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<void(QList<KMime::Message::Ptr>)> callback)
{
auto composer = new ComposerJob;
fillComposer(composer);
composer->setAutoSave(true);
m_composers.append(composer);
connect(composer, &MessageComposer::ComposerJob::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 ComposerJob;
fillComposer(composer);
composer->setAutoSave(true);
+ composer->setSignAndEncrypt(false, true);
+
+ const auto normalized = GpgME::UserID::addrSpecFromString(m_identity.fullEmailAddr().toUtf8().constData());
+ const auto normalizedSender = QString::fromUtf8(normalized.c_str());
+
+ auto encryptionKeys = Kleo::KeyCache::instance()->findByEMailAddress(normalizedSender.toStdString());
+ std::vector<GpgME::Key> filteredEncryptionKeys;
+
+ Kleo::CryptoMessageFormat format = Kleo::SMIMEFormat;
+
+ // ensure all keys are valid
+ for (const auto &key : encryptionKeys) {
+ if (strlen(key.userID(0).email()) > 0) {
+ filteredEncryptionKeys.push_back(key);
+ if (key.protocol() == GpgME::OpenPGP && key.canEncrypt()) {
+ format = Kleo::OpenPGPMIMEFormat;
+ }
+ qWarning() << key.protocol() << key.canEncrypt() << key.userID(0).email();
+ }
+ }
+ Q_ASSERT(!filteredEncryptionKeys.empty());
+
+ composer->setEncryptionKeys({QPair<QStringList, std::vector<GpgME::Key>>{{normalizedSender}, filteredEncryptionKeys}});
+ composer->setCryptoMessageFormat(format);
m_composers.append(composer);
connect(composer, &MessageComposer::ComposerJob::result, this, &ComposerViewBase::slotAutoSaveComposeResult);
composer->start();
}
void ComposerViewBase::slotSaveDraft()
{
qCDebug(EDITOR_LOG) << "Saving draft";
m_composers.clear();
auto composer = new ComposerJob;
fillComposer(composer);
composer->setDraft(true);
composer->setSignAndEncrypt(false, true);
const auto normalized = GpgME::UserID::addrSpecFromString(m_identity.fullEmailAddr().toUtf8().constData());
const auto normalizedSender = QString::fromUtf8(normalized.c_str());
auto encryptionKeys = Kleo::KeyCache::instance()->findByEMailAddress(normalizedSender.toStdString());
std::vector<GpgME::Key> filteredEncryptionKeys;
// ensure all keys are valid
for (const auto &key : encryptionKeys) {
if (strlen(key.userID(0).email()) > 0) {
filteredEncryptionKeys.push_back(key);
}
}
Q_ASSERT(!filteredEncryptionKeys.empty());
composer->setEncryptionKeys({QPair<QStringList, std::vector<GpgME::Key>>{{normalizedSender}, filteredEncryptionKeys}});
composer->setCryptoMessageFormat(m_cryptoMessageFormat);
m_composers.append(composer);
connect(composer, &MessageComposer::ComposerJob::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::ComposerJob;
Q_ASSERT(dynamic_cast<ComposerJob *>(job));
auto composer = static_cast<ComposerJob *>(job);
if (composer->error() == ComposerJob::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::ComposerJob::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::ComposerJob;
Q_ASSERT(dynamic_cast<ComposerJob *>(job));
auto composer = static_cast<ComposerJob *>(job);
if (composer->error() == ComposerJob::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 closeWindow();
} else if (composer->error() == MessageComposer::ComposerJob::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"), Draft);
} else {
qCDebug(EDITOR_LOG) << "other Error." << composer->error();
Q_EMIT failed(i18n("Could not save message as draft: %1", job->errorString()), Draft);
}
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<qint64>(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<qint64>(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() << "mailId=" << mailId();
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 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->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(QByteArrayView("name"))) {
part->setName(ct->parameter(QByteArrayView("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::queueMessage(const KMime::Message::Ptr &message)
{
Q_ASSERT(m_messageDispatcher);
m_messageDispatcher->dispatch(message, from(), mailId());
}
void ComposerViewBase::fillComposer(MessageComposer::ComposerJob *composer)
{
fillComposer(composer, UseUnExpandedRecipients, false);
}
void ComposerViewBase::fillComposer(MessageComposer::ComposerJob *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 {
qCDebug(EDITOR_LOG) << "fillComposer has no attachment 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<KIdentityManagementCore::Identity &>(oldIdent).signature();
KIdentityManagementCore::Signature newSig = const_cast<KIdentityManagementCore::Identity &>(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::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<Kleo::ExpiryChecker> 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::mailIdIsEmpty() const
{
return m_autoSaveUUID.isEmpty();
}
MessageDispatcher *ComposerViewBase::messageDispatcher() const
{
return m_messageDispatcher;
}
void ComposerViewBase::setMessageDispatcher(MessageDispatcher *messageDispatcher)
{
m_messageDispatcher = messageDispatcher;
}
#include "moc_composerviewbase.cpp"
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Thu, Nov 6, 3:18 PM (4 h, 3 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
93/fb/39de143b4983b9e7846ddf5060c9
Attached To
rOJ GpgOL.js
Event Timeline
Log In to Comment