Page MenuHome GnuPG

No OneTemporary

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

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

Event Timeline