Page Menu
Home
GnuPG
Search
Configure Global Search
Log In
Files
F26446302
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Size
50 KB
Subscribers
None
View Options
diff --git a/src/core/messageparser.cpp b/src/core/messageparser.cpp
index 0834678..d7954ba 100644
--- a/src/core/messageparser.cpp
+++ b/src/core/messageparser.cpp
@@ -1,242 +1,241 @@
// SPDX-FileCopyrightText: 2016 Christian Mollekopf <mollekopf@kolabsys.com>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "messageparser.h"
#include "attachmentmodel.h"
#include "mimetreeparser_core_debug.h"
#include "objecttreeparser.h"
#include "partmodel.h"
#include <KLocalizedString>
#include <QElapsedTimer>
namespace
{
template<typename T>
const T *findHeader(KMime::Content *content, KMime::Content *protectedHeaderNode)
{
if (protectedHeaderNode) {
auto header = protectedHeaderNode->header<T>();
if (header) {
return header;
}
}
auto header = content->header<T>();
if (header || !content->parent()) {
return header;
}
return findHeader<T>(content->parent(), nullptr);
}
QString mailboxesToHtml(const KMime::Types::Mailbox::List &mailboxes)
{
QStringList html;
for (const auto &mailbox : mailboxes) {
if (mailbox.hasName() && mailbox.hasAddress()) {
- html << QStringLiteral("%1 <<a href=\"mailto:%2\">%2</a>>")
- .arg(mailbox.name().toHtmlEscaped(), QString::fromUtf8(mailbox.address()).toHtmlEscaped());
+ html << QStringLiteral("%1 <%2>").arg(mailbox.name().toHtmlEscaped(), QString::fromUtf8(mailbox.address()).toHtmlEscaped());
} else if (mailbox.hasAddress()) {
- html << QStringLiteral("<a href=\"mailto:%2\">%2</a>").arg(QString::fromUtf8(mailbox.address()));
+ html << QString::fromUtf8(mailbox.address());
} else {
if (mailbox.hasName()) {
html << mailbox.name();
} else {
Q_ASSERT_X(false, __FUNCTION__, "Mailbox does not contains email address nor name");
html << i18nc("Displayed when a CC, FROM or TO field in an email is empty", "Unknown");
}
}
}
return html.join(i18nc("list separator", ", "));
}
}
class MessagePartPrivate
{
public:
std::shared_ptr<MimeTreeParser::ObjectTreeParser> mParser;
KMime::Message::Ptr mMessage;
KMime::Content *protectedHeaderNode = nullptr;
std::unique_ptr<KMime::Content> ownedContent;
};
MessageParser::MessageParser(QObject *parent)
: QObject(parent)
, d(std::unique_ptr<MessagePartPrivate>(new MessagePartPrivate))
{
}
MessageParser::~MessageParser()
{
}
KMime::Message::Ptr MessageParser::message() const
{
return d->mMessage;
}
void MessageParser::setMessage(const KMime::Message::Ptr message)
{
if (message == d->mMessage) {
return;
}
if (!message) {
qCWarning(MIMETREEPARSER_CORE_LOG) << Q_FUNC_INFO << "Empty message given";
return;
}
d->mMessage = message;
QElapsedTimer time;
time.start();
auto parser = std::make_shared<MimeTreeParser::ObjectTreeParser>();
parser->parseObjectTree(message.data());
qCDebug(MIMETREEPARSER_CORE_LOG) << "Message parsing took: " << time.elapsed();
parser->decryptParts();
qCDebug(MIMETREEPARSER_CORE_LOG) << "Message parsing and decryption/verification: " << time.elapsed();
d->mParser = parser;
const auto contentParts = parser->collectContentParts();
for (const auto &part : contentParts) {
if (!part->node()) {
continue;
}
const auto contentType = part->node()->contentType();
if (contentType && contentType->hasParameter(QStringLiteral("protected-headers"))) {
const auto contentDisposition = part->node()->contentDisposition();
// Check for legacy format for protected-headers
if (contentDisposition && contentDisposition->disposition() == KMime::Headers::CDinline) {
d->ownedContent = std::make_unique<KMime::Content>();
// we put the decoded content as encoded content of the new node
// as the header are inline in part->node()
d->ownedContent->setContent(part->node()->decodedContent());
d->ownedContent->parse();
d->protectedHeaderNode = d->ownedContent.get();
break;
}
d->protectedHeaderNode = part->node();
break;
}
}
Q_EMIT htmlChanged();
}
bool MessageParser::loaded() const
{
return bool{d->mParser};
}
QString MessageParser::structureAsString() const
{
if (!d->mParser) {
return {};
}
return d->mParser->structureAsString();
}
PartModel *MessageParser::parts() const
{
if (!d->mParser) {
return nullptr;
}
const auto model = new PartModel(d->mParser);
return model;
}
AttachmentModel *MessageParser::attachments() const
{
if (!d->mParser) {
return nullptr;
}
auto attachmentModel = new AttachmentModel(d->mParser);
attachmentModel->setParent(const_cast<MessageParser *>(this));
return attachmentModel;
}
QString MessageParser::subject() const
{
if (d->mMessage) {
const auto header = findHeader<KMime::Headers::Subject>(d->mMessage.get(), d->protectedHeaderNode);
if (header) {
return header->asUnicodeString();
}
}
return QString();
}
QString MessageParser::from() const
{
if (d->mMessage) {
const auto header = findHeader<KMime::Headers::From>(d->mMessage.get(), d->protectedHeaderNode);
if (!header) {
return {};
}
return mailboxesToHtml(header->mailboxes());
}
return QString();
}
QString MessageParser::sender() const
{
if (d->mMessage) {
const auto header = findHeader<KMime::Headers::Sender>(d->mMessage.get(), d->protectedHeaderNode);
if (!header) {
return {};
}
return mailboxesToHtml(header->mailboxes());
}
return QString();
}
QString MessageParser::to() const
{
if (d->mMessage) {
const auto header = findHeader<KMime::Headers::To>(d->mMessage.get(), d->protectedHeaderNode);
if (!header) {
return {};
}
return mailboxesToHtml(header->mailboxes());
}
return QString();
}
QString MessageParser::cc() const
{
if (d->mMessage) {
const auto header = findHeader<KMime::Headers::Cc>(d->mMessage.get(), d->protectedHeaderNode);
if (!header) {
return {};
}
return mailboxesToHtml(header->mailboxes());
}
return QString();
}
QString MessageParser::bcc() const
{
if (d->mMessage) {
const auto header = findHeader<KMime::Headers::Bcc>(d->mMessage.get(), d->protectedHeaderNode);
if (!header) {
return {};
}
return mailboxesToHtml(header->mailboxes());
}
return QString();
}
QDateTime MessageParser::date() const
{
if (d->mMessage) {
const auto header = findHeader<KMime::Headers::Date>(d->mMessage.get(), d->protectedHeaderNode);
if (header) {
return header->dateTime();
}
}
return QDateTime();
}
diff --git a/src/core/utils.cpp b/src/core/utils.cpp
index 4d0fd05..81a558b 100644
--- a/src/core/utils.cpp
+++ b/src/core/utils.cpp
@@ -1,61 +1,63 @@
// SPDX-FileCopyrightText: 2016 Sandro Knauß <sknauss@kde.org>
// SPDX-FileCopyrightText: 2023 g10 Code GmbH
// SPDX-FileContributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "utils.h"
#include <KLocalizedString>
+#include <Libkleo/Formatting>
#include <libkleo/dn.h>
using namespace MimeTreeParser;
KMime::Content *MimeTreeParser::findTypeInDirectChildren(KMime::Content *content, const QByteArray &mimeType)
{
const auto contents = content->contents();
for (const auto child : contents) {
if ((!child->contentType()->isEmpty()) && (mimeType == child->contentType()->mimeType())) {
return child;
}
}
return nullptr;
}
QString MimeTreeParser::decryptRecipientsToHtml(const std::vector<std::pair<GpgME::DecryptionResult::Recipient, GpgME::Key>> &recipients,
const QGpgME::Protocol *cryptoProto)
{
QString text = QStringLiteral("<ul>");
for (const auto &recipientIt : recipients) {
const auto recipient = recipientIt.first;
const auto key = recipientIt.second;
if (key.keyID()) {
QString displayName = QString::fromLatin1(key.userID(0).id());
if (cryptoProto == QGpgME::smime()) {
Kleo::DN dn(displayName);
displayName = dnToDisplayName(dn);
}
displayName = displayName.toHtmlEscaped();
const auto link = QStringLiteral("messageviewer:showCertificate#%1 ### %2 ### %3")
.arg(cryptoProto->displayName(), cryptoProto->name(), QString::fromLatin1(key.keyID()));
- text += QStringLiteral("<li>%1 (<a href=\"%2\">0x%3</a>)</li>").arg(displayName, link, QString::fromLatin1(key.keyID()));
+ text += QStringLiteral("<li>%1 (<a href=\"%2\">%3</a>)</li>").arg(displayName, link, Kleo::Formatting::prettyID(key.keyID()));
} else {
const auto link = QStringLiteral("messageviewer:showCertificate#%1 ### %2 ### %3")
.arg(cryptoProto->displayName(), cryptoProto->name(), QString::fromLatin1(recipient.keyID()));
- text += QStringLiteral("<li>%1 (<a href=\"%2\">0x%3</a>)</li>").arg(i18nc("@info", "Unknow Key"), link, QString::fromLatin1(recipient.keyID()));
+ text +=
+ QStringLiteral("<li>%1 (<a href=\"%2\">%3</a>)</li>").arg(i18nc("@info", "Unknow Key"), link, Kleo::Formatting::prettyID(recipient.keyID()));
}
}
text += QStringLiteral("</ul>");
return text;
}
QString MimeTreeParser::dnToDisplayName(const Kleo::DN &dn)
{
QString displayName = dn[QStringLiteral("CN")];
if (displayName.isEmpty()) {
// In case there is no CN, put the full DN as display name
displayName = dn.prettyDN();
} else if (!dn[QStringLiteral("O")].isEmpty()) {
displayName += i18nc("Separator", " - ") + dn[QStringLiteral("O")];
}
return displayName;
}
diff --git a/src/widgets/messagecontainerwidget.cpp b/src/widgets/messagecontainerwidget.cpp
index 029efbf..19a0d49 100644
--- a/src/widgets/messagecontainerwidget.cpp
+++ b/src/widgets/messagecontainerwidget.cpp
@@ -1,252 +1,252 @@
// SPDX-FileCopyrightText: 2023 g10 Code GmbH
// SPDX-FileContributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "../core/utils.h"
#include "messagecontainerwidget_p.h"
#include "urlhandler_p.h"
#include <KLocalizedString>
#include <KMessageWidget>
#include <Libkleo/Compliance>
+#include <Libkleo/Formatting>
#include <QGpgME/Protocol>
-#include <QDebug>
#include <QLabel>
#include <QPaintEvent>
#include <QPainter>
#include <QResizeEvent>
#include <QVBoxLayout>
namespace
{
const int borderWidth = 5;
QColor getColor(PartModel::SecurityLevel securityLevel)
{
const static QHash<PartModel::SecurityLevel, QColor> colors{
{PartModel::Good, QColor(39, 174, 96)}, // Window: ForegroundPositive
{PartModel::Bad, QColor(218, 68, 83)}, // Window: ForegroundNegative
{PartModel::NotSoGood, QColor(246, 116, 0)}, // Window: ForegroundNeutral
};
return colors.value(securityLevel, QColor());
}
KMessageWidget::MessageType getType(PartModel::SecurityLevel securityLevel)
{
const static QHash<PartModel::SecurityLevel, KMessageWidget::MessageType> messageTypes{
{PartModel::Good, KMessageWidget::MessageType::Positive},
{PartModel::Bad, KMessageWidget::MessageType::Error},
{PartModel::NotSoGood, KMessageWidget::MessageType::Warning},
};
return messageTypes.value(securityLevel, KMessageWidget::MessageType::Information);
}
QString getDetails(const SignatureInfo &signatureDetails)
{
QString href;
if (signatureDetails.cryptoProto) {
href = QStringLiteral("messageviewer:showCertificate#%1 ### %2 ### %3")
.arg(signatureDetails.cryptoProto->displayName(), signatureDetails.cryptoProto->name(), QString::fromLatin1(signatureDetails.keyId));
}
QString details;
if (signatureDetails.keyMissing) {
if (Kleo::DeVSCompliance::isCompliant() && signatureDetails.isCompliant) {
details += i18ndc("mimetreeparser",
"@label",
"This message has been signed VS-NfD compliant using the key <a href=\"%1\">%2</a>.",
href,
- QString::fromUtf8(signatureDetails.keyId))
+ Kleo::Formatting::prettyID(signatureDetails.keyId.toStdString().data()))
+ QLatin1Char('\n');
} else {
details += i18ndc("mimetreeparser",
"@label",
"This message has been signed using the key <a href=\"%1\">%2</a>.",
href,
- QString::fromUtf8(signatureDetails.keyId))
+ Kleo::Formatting::prettyID(signatureDetails.keyId.toStdString().data()))
+ QLatin1Char('\n');
}
details += i18ndc("mimetreeparser", "@label", "The key details are not available.");
} else {
QString signerDisplayName = signatureDetails.signer.toHtmlEscaped();
if (signatureDetails.cryptoProto == QGpgME::smime()) {
Kleo::DN dn(signatureDetails.signer);
signerDisplayName = MimeTreeParser::dnToDisplayName(dn).toHtmlEscaped();
}
if (Kleo::DeVSCompliance::isCompliant() && signatureDetails.isCompliant) {
details += i18ndc("mimetreeparser", "@label", "This message has been signed VS-NfD compliant by %1.", signerDisplayName);
} else {
details += i18ndc("mimetreeparser", "@label", "This message has been signed by %1.", signerDisplayName);
}
if (signatureDetails.keyRevoked) {
details += QLatin1Char('\n') + i18ndc("mimetreeparser", "@label", "The <a href=\"%1\">key</a> was revoked.", href);
}
if (signatureDetails.keyExpired) {
details += QLatin1Char('\n') + i18ndc("mimetreeparser", "@label", "The <a href=\"%1\">key</a> was expired.", href);
}
if (signatureDetails.keyTrust == GpgME::Signature::Unknown) {
details +=
QLatin1Char(' ') + i18ndc("mimetreeparser", "@label", "The signature is valid, but the <a href=\"%1\">key</a>'s validity is unknown.", href);
} else if (signatureDetails.keyTrust == GpgME::Signature::Marginal) {
details +=
QLatin1Char(' ') + i18ndc("mimetreeparser", "@label", "The signature is valid and the <a href=\"%1\">key</a> is marginally trusted.", href);
} else if (signatureDetails.keyTrust == GpgME::Signature::Full) {
details += QLatin1Char(' ') + i18ndc("mimetreeparser", "@label", "The signature is valid and the <a href=\"%1\">key</a> is fully trusted.", href);
} else if (signatureDetails.keyTrust == GpgME::Signature::Ultimate) {
details +=
QLatin1Char(' ') + i18ndc("mimetreeparser", "@label", "The signature is valid and the <a href=\"%1\">key</a> is ultimately trusted.", href);
} else {
details += QLatin1Char(' ') + i18ndc("mimetreeparser", "@label", "The signature is valid, but the <a href=\"%1\">key</a> is untrusted.", href);
}
if (!signatureDetails.signatureIsGood && !signatureDetails.keyRevoked && !signatureDetails.keyExpired
&& signatureDetails.keyTrust != GpgME::Signature::Unknown) {
details += QLatin1Char(' ') + i18ndc("mimetreeparser", "@label", "The signature is invalid.");
}
}
return details;
}
}
MessageWidgetContainer::MessageWidgetContainer(bool isSigned,
const SignatureInfo &signatureInfo,
PartModel::SecurityLevel signatureSecurityLevel,
bool displaySignatureInfo,
bool isEncrypted,
const SignatureInfo &encryptionInfo,
PartModel::SecurityLevel encryptionSecurityLevel,
bool displayEncryptionInfo,
UrlHandler *urlHandler,
QWidget *parent)
: QFrame(parent)
, m_isSigned(isSigned)
, m_signatureInfo(signatureInfo)
, m_signatureSecurityLevel(signatureSecurityLevel)
, m_displaySignatureInfo(displaySignatureInfo)
, m_isEncrypted(isEncrypted)
, m_encryptionInfo(encryptionInfo)
, m_encryptionSecurityLevel(encryptionSecurityLevel)
, m_displayEncryptionInfo(displayEncryptionInfo)
, m_urlHandler(urlHandler)
{
createLayout();
}
MessageWidgetContainer::~MessageWidgetContainer() = default;
void MessageWidgetContainer::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event)
if (!m_isSigned && !m_isEncrypted) {
return;
}
QPainter painter(this);
if (layoutDirection() == Qt::RightToLeft) {
auto r = rect();
r.setX(width() - borderWidth);
r.setWidth(borderWidth);
const QColor color = getColor(PartModel::SecurityLevel::Good);
painter.setRenderHint(QPainter::Antialiasing);
painter.setBrush(QColor(color));
painter.setPen(QPen(Qt::NoPen));
painter.drawRect(r);
} else {
auto r = rect();
r.setWidth(borderWidth);
const QColor color = getColor(PartModel::SecurityLevel::Good);
painter.setRenderHint(QPainter::Antialiasing);
painter.setBrush(QColor(color));
painter.setPen(QPen(Qt::NoPen));
painter.drawRect(r);
}
}
bool MessageWidgetContainer::event(QEvent *event)
{
if (event->type() == QEvent::Polish && !layout()) {
createLayout();
}
return QFrame::event(event);
}
void MessageWidgetContainer::createLayout()
{
delete layout();
auto vLayout = new QVBoxLayout(this);
if (m_isSigned || m_isEncrypted) {
if (layoutDirection() == Qt::RightToLeft) {
layout()->setContentsMargins(0, 0, borderWidth * 2, 0);
} else {
layout()->setContentsMargins(borderWidth * 2, 0, 0, 0);
}
}
if (m_isEncrypted && m_displayEncryptionInfo) {
auto encryptionMessage = new KMessageWidget(this);
encryptionMessage->setObjectName(QStringLiteral("EncryptionMessage"));
encryptionMessage->setCloseButtonVisible(false);
encryptionMessage->setIcon(QIcon::fromTheme(QStringLiteral("mail-encrypted")));
QString text;
if (m_encryptionInfo.keyId.isEmpty()) {
encryptionMessage->setMessageType(KMessageWidget::Error);
if (Kleo::DeVSCompliance::isCompliant() && m_encryptionInfo.isCompliant) {
text = i18n("This message is VS-NfD compliant encrypted but we don't have the key for it.", QString::fromUtf8(m_encryptionInfo.keyId));
} else {
text = i18n("This message is encrypted but we don't have the key for it.");
}
} else {
encryptionMessage->setMessageType(KMessageWidget::Positive);
if (Kleo::DeVSCompliance::isCompliant() && m_encryptionInfo.isCompliant) {
text = i18n("This message is VS-NfD compliant encrypted.");
} else {
text = i18n("This message is encrypted.");
}
}
encryptionMessage->setText(text + QLatin1Char(' ') + QStringLiteral("<a href=\"messageviewer:showDetails\">Details</a>"));
connect(encryptionMessage, &KMessageWidget::linkActivated, this, [this, encryptionMessage, text](const QString &link) {
QUrl url(link);
if (url.path() == QStringLiteral("showDetails")) {
QString newText = text + QLatin1Char(' ') + i18n("The message is encrypted for the following keys:");
newText += MimeTreeParser::decryptRecipientsToHtml(m_encryptionInfo.decryptRecipients, m_encryptionInfo.cryptoProto);
encryptionMessage->setText(newText);
return;
}
if (url.path() == QStringLiteral("showCertificate")) {
m_urlHandler->handleClick(QUrl(link), window()->windowHandle());
}
});
vLayout->addWidget(encryptionMessage);
}
if (m_isSigned && m_displaySignatureInfo) {
auto signatureMessage = new KMessageWidget(this);
signatureMessage->setObjectName(QStringLiteral("SignatureMessage"));
signatureMessage->setIcon(QIcon::fromTheme(QStringLiteral("mail-signed")));
signatureMessage->setCloseButtonVisible(false);
signatureMessage->setText(getDetails(m_signatureInfo));
connect(signatureMessage, &KMessageWidget::linkActivated, this, [this](const QString &link) {
m_urlHandler->handleClick(QUrl(link), window()->windowHandle());
});
signatureMessage->setMessageType(getType(m_signatureSecurityLevel));
signatureMessage->setWordWrap(true);
vLayout->addWidget(signatureMessage);
}
}
diff --git a/src/widgets/messageviewer.cpp b/src/widgets/messageviewer.cpp
index 7f0c849..14e6a07 100644
--- a/src/widgets/messageviewer.cpp
+++ b/src/widgets/messageviewer.cpp
@@ -1,422 +1,441 @@
// SPDX-FileCopyrightText: 2023 Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "messageviewer.h"
#include "attachmentview_p.h"
#include "messagecontainerwidget_p.h"
#include "mimetreeparser_widgets_debug.h"
#include "urlhandler_p.h"
#include <KCalendarCore/Event>
#include <KCalendarCore/ICalFormat>
#include <KCalendarCore/Incidence>
#include <KMessageWidget>
#include <KLocalizedString>
#include <MimeTreeParserCore/AttachmentModel>
#include <MimeTreeParserCore/MessageParser>
#include <MimeTreeParserCore/ObjectTreeParser>
#include <MimeTreeParserCore/PartModel>
#include <QAction>
#include <QDesktopServices>
#include <QFileDialog>
#include <QFormLayout>
#include <QGroupBox>
#include <QLabel>
#include <QMenu>
#include <QScrollArea>
#include <QSplitter>
#include <QStandardPaths>
#include <QVBoxLayout>
+#include <qnamespace.h>
using namespace MimeTreeParser::Widgets;
class MessageViewer::Private
{
public:
Private(MessageViewer *q_ptr)
: q{q_ptr}
, messageWidget(new KMessageWidget(q_ptr))
{
createActions();
messageWidget->setCloseButtonVisible(true);
messageWidget->hide();
}
MessageViewer *q;
QVBoxLayout *layout = nullptr;
KMime::Message::Ptr message;
MessageParser parser;
QScrollArea *scrollArea = nullptr;
QFormLayout *formLayout = nullptr;
AttachmentView *attachmentView = nullptr;
MimeTreeParser::MessagePart::List selectedParts;
UrlHandler *urlHandler = nullptr;
KMessageWidget *const messageWidget = nullptr;
QAction *saveAttachmentAction = nullptr;
QAction *openAttachmentAction = nullptr;
QAction *importPublicKeyAction = nullptr;
void createActions()
{
saveAttachmentAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("&Save Attachment As..."), q);
connect(saveAttachmentAction, &QAction::triggered, q, [this]() {
saveSelectedAttachments();
});
openAttachmentAction = new QAction(i18nc("to open", "Open"), q);
connect(openAttachmentAction, &QAction::triggered, q, [this]() {
openSelectedAttachments();
});
importPublicKeyAction = new QAction(i18nc("@action:inmenu", "Import public key"), q);
connect(importPublicKeyAction, &QAction::triggered, q, [this]() {
importPublicKey();
});
}
void openSelectedAttachments();
void saveSelectedAttachments();
void selectionChanged();
void showContextMenu();
void importPublicKey();
void recursiveBuildViewer(PartModel *parts, QVBoxLayout *layout, const QModelIndex &parent);
};
void MessageViewer::Private::openSelectedAttachments()
{
Q_ASSERT(selectedParts.count() >= 1);
for (const auto &part : std::as_const(selectedParts)) {
parser.attachments()->openAttachment(part);
}
}
void MessageViewer::Private::saveSelectedAttachments()
{
Q_ASSERT(selectedParts.count() >= 1);
for (const auto &part : std::as_const(selectedParts)) {
QString pname = part->filename();
if (pname.isEmpty()) {
pname = i18nc("Fallback when file has no name", "unnamed");
}
const QString path = QFileDialog::getSaveFileName(q, i18n("Save Attachment As"), pname);
parser.attachments()->saveAttachmentToPath(part, path);
}
}
void MessageViewer::Private::importPublicKey()
{
Q_ASSERT(selectedParts.count() == 1);
parser.attachments()->importPublicKey(selectedParts[0]);
}
void MessageViewer::Private::showContextMenu()
{
const int numberOfParts(selectedParts.count());
QMenu menu;
if (numberOfParts == 1) {
const QString mimetype = QString::fromLatin1(selectedParts.first()->mimeType());
if (mimetype == QLatin1String("application/pgp-keys")) {
menu.addAction(importPublicKeyAction);
}
}
menu.addAction(openAttachmentAction);
menu.addAction(saveAttachmentAction);
menu.exec(QCursor::pos());
}
void MessageViewer::Private::selectionChanged()
{
const QModelIndexList selectedRows = attachmentView->selectionModel()->selectedRows();
MimeTreeParser::MessagePart::List selectedParts;
selectedParts.reserve(selectedRows.count());
for (const QModelIndex &index : selectedRows) {
auto part = attachmentView->model()->data(index, AttachmentModel::AttachmentPartRole).value<MimeTreeParser::MessagePart::Ptr>();
selectedParts.append(part);
}
this->selectedParts = selectedParts;
}
MessageViewer::MessageViewer(QWidget *parent)
: QSplitter(Qt::Vertical, parent)
, d(std::make_unique<MessageViewer::Private>(this))
{
setObjectName(QStringLiteral("MessageViewerSplitter"));
setChildrenCollapsible(false);
setSizes({0});
addWidget(d->messageWidget);
- auto headersArea = new QWidget(this);
- headersArea->setSizePolicy(sizePolicy().horizontalPolicy(), QSizePolicy::Expanding);
- addWidget(headersArea);
+ auto mainWidget = new QWidget(this);
+ auto mainLayout = new QVBoxLayout(mainWidget);
+ mainLayout->setContentsMargins({});
+ mainLayout->setSpacing(0);
+
+ auto headersArea = new QWidget(mainWidget);
+ headersArea->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
+ mainLayout->addWidget(headersArea);
d->urlHandler = new UrlHandler(this);
d->formLayout = new QFormLayout(headersArea);
auto widget = new QWidget(this);
d->layout = new QVBoxLayout(widget);
d->layout->setSizeConstraint(QLayout::SetMinAndMaxSize);
d->layout->setObjectName(QStringLiteral("PartLayout"));
d->scrollArea = new QScrollArea(this);
d->scrollArea->setWidget(widget);
d->scrollArea->setWidgetResizable(true);
d->scrollArea->setBackgroundRole(QPalette::Base);
- addWidget(d->scrollArea);
- setStretchFactor(2, 2);
+ mainLayout->addWidget(d->scrollArea);
+ mainLayout->setStretchFactor(d->scrollArea, 2);
+ setStretchFactor(1, 2);
d->attachmentView = new AttachmentView(this);
d->attachmentView->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
addWidget(d->attachmentView);
connect(d->attachmentView, &AttachmentView::contextMenuRequested, this, [this] {
d->selectionChanged();
d->showContextMenu();
});
-
- setMinimumSize(600, 600);
}
MessageViewer::~MessageViewer()
{
QLayoutItem *child;
while ((child = d->layout->takeAt(0)) != nullptr) {
delete child->widget();
delete child;
}
}
KMime::Message::Ptr MessageViewer::message() const
{
return d->parser.message();
}
void MessageViewer::Private::recursiveBuildViewer(PartModel *parts, QVBoxLayout *layout, const QModelIndex &parent)
{
for (int i = 0, count = parts->rowCount(parent); i < count; i++) {
const auto type = static_cast<PartModel::Types>(parts->data(parts->index(i, 0, parent), PartModel::TypeRole).toUInt());
const auto content = parts->data(parts->index(i, 0, parent), PartModel::ContentRole).toString();
const auto signatureInfo = parts->data(parts->index(i, 0, parent), PartModel::SignatureDetails).value<SignatureInfo>();
const auto isSigned = parts->data(parts->index(i, 0, parent), PartModel::IsSignedRole).toBool();
const auto signatureSecurityLevel =
static_cast<PartModel::SecurityLevel>(parts->data(parts->index(i, 0, parent), PartModel::SignatureSecurityLevelRole).toInt());
const auto encryptionInfo = parts->data(parts->index(i, 0, parent), PartModel::EncryptionDetails).value<SignatureInfo>();
const auto isEncrypted = parts->data(parts->index(i, 0, parent), PartModel::IsEncryptedRole).toBool();
const auto encryptionSecurityLevel =
static_cast<PartModel::SecurityLevel>(parts->data(parts->index(i, 0, parent), PartModel::EncryptionSecurityLevelRole).toInt());
const auto displayEncryptionInfo =
i == 0 || parts->data(parts->index(i - 1, 0, parent), PartModel::EncryptionDetails).value<SignatureInfo>().keyId != encryptionInfo.keyId;
const auto displaySignatureInfo =
i == 0 || parts->data(parts->index(i - 1, 0, parent), PartModel::SignatureDetails).value<SignatureInfo>().keyId != signatureInfo.keyId;
switch (type) {
case PartModel::Types::Plain: {
auto container = new MessageWidgetContainer(isSigned,
signatureInfo,
signatureSecurityLevel,
displaySignatureInfo,
isEncrypted,
encryptionInfo,
encryptionSecurityLevel,
displayEncryptionInfo,
urlHandler);
auto label = new QLabel(content);
label->setTextInteractionFlags(Qt::TextBrowserInteraction);
label->setOpenExternalLinks(true);
label->setWordWrap(true);
container->layout()->addWidget(label);
layout->addWidget(container);
break;
}
case PartModel::Types::Ical: {
auto container = new MessageWidgetContainer(isSigned,
signatureInfo,
signatureSecurityLevel,
displaySignatureInfo,
isEncrypted,
encryptionInfo,
encryptionSecurityLevel,
displayEncryptionInfo,
urlHandler);
KCalendarCore::ICalFormat format;
auto incidence = format.fromString(content);
auto widget = new QGroupBox(container);
widget->setTitle(i18n("Invitation"));
auto incidenceLayout = new QFormLayout(widget);
incidenceLayout->addRow(i18n("&Summary:"), new QLabel(incidence->summary()));
incidenceLayout->addRow(i18n("&Organizer:"), new QLabel(incidence->organizer().fullName()));
if (incidence->location().length() > 0) {
incidenceLayout->addRow(i18n("&Location:"), new QLabel(incidence->location()));
}
incidenceLayout->addRow(i18n("&Start date:"), new QLabel(incidence->dtStart().toLocalTime().toString()));
if (const auto event = incidence.dynamicCast<KCalendarCore::Event>()) {
incidenceLayout->addRow(i18n("&End date:"), new QLabel(event->dtEnd().toLocalTime().toString()));
}
if (incidence->description().length() > 0) {
incidenceLayout->addRow(i18n("&Details:"), new QLabel(incidence->description()));
}
container->layout()->addWidget(widget);
layout->addWidget(container);
break;
}
case PartModel::Types::Encapsulated: {
auto container = new MessageWidgetContainer(isSigned,
signatureInfo,
signatureSecurityLevel,
displaySignatureInfo,
isEncrypted,
encryptionInfo,
encryptionSecurityLevel,
displayEncryptionInfo,
urlHandler);
auto groupBox = new QGroupBox(container);
groupBox->setSizePolicy(QSizePolicy::MinimumExpanding, q->sizePolicy().verticalPolicy());
groupBox->setTitle(i18n("Encapsulated email"));
auto encapsulatedLayout = new QVBoxLayout(groupBox);
auto header = new QWidget(groupBox);
auto headerLayout = new QFormLayout(header);
const auto from = parts->data(parts->index(i, 0, parent), PartModel::SenderRole).toString();
const auto date = parts->data(parts->index(i, 0, parent), PartModel::DateRole).toDateTime();
headerLayout->addRow(i18n("From:"), new QLabel(from));
headerLayout->addRow(i18n("Date:"), new QLabel(date.toLocalTime().toString()));
encapsulatedLayout->addWidget(header);
recursiveBuildViewer(parts, encapsulatedLayout, parts->index(i, 0, parent));
container->layout()->addWidget(groupBox);
layout->addWidget(container);
break;
}
case PartModel::Types::Error: {
const auto errorString = parts->data(parts->index(i, 0, parent), PartModel::ErrorString).toString();
auto errorWidget = new KMessageWidget(errorString);
errorWidget->setCloseButtonVisible(false);
errorWidget->setMessageType(KMessageWidget::MessageType::Error);
QObject::connect(errorWidget, &KMessageWidget::linkActivated, errorWidget, [this, errorWidget](const QString &link) {
QUrl url(link);
if (url.path() == QStringLiteral("showCertificate")) {
urlHandler->handleClick(QUrl(link), errorWidget->window()->windowHandle());
}
});
errorWidget->setWordWrap(true);
layout->addWidget(errorWidget);
break;
}
default:
qCWarning(MIMETREEPARSER_WIDGET_LOG) << parts->data(parts->index(i, 0, parent), PartModel::ContentRole) << type;
}
}
}
-static QLabel *createLabel(const QString &content)
+class HeaderLabel : public QLabel
{
- const auto label = new QLabel(content);
- label->setTextInteractionFlags(Qt::TextBrowserInteraction);
- label->setOpenExternalLinks(true);
- return label;
-}
+public:
+ HeaderLabel(const QString &content)
+ : QLabel(content)
+ {
+ setWordWrap(true);
+ setTextFormat(Qt::PlainText);
+ setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
+ }
+
+ void resizeEvent(QResizeEvent *event) override
+ {
+ int height = heightForWidth(width());
+ setMaximumHeight(height);
+ setMinimumHeight(height);
+
+ QLabel::resizeEvent(event);
+ }
+};
void MessageViewer::setMessage(const KMime::Message::Ptr message)
{
setUpdatesEnabled(false);
d->parser.setMessage(message);
connect(d->parser.attachments(), &AttachmentModel::info, this, [this](const QString &message) {
d->messageWidget->setMessageType(KMessageWidget::Information);
d->messageWidget->setText(message);
d->messageWidget->animatedShow();
});
connect(d->parser.attachments(), &AttachmentModel::errorOccurred, this, [this](const QString &message) {
d->messageWidget->setMessageType(KMessageWidget::Error);
d->messageWidget->setText(message);
d->messageWidget->animatedShow();
});
for (int i = d->formLayout->rowCount() - 1; i >= 0; i--) {
d->formLayout->removeRow(i);
}
if (!d->parser.subject().isEmpty()) {
const auto label = new QLabel(d->parser.subject());
label->setTextFormat(Qt::PlainText);
d->formLayout->addRow(i18n("&Subject:"), label);
}
if (!d->parser.from().isEmpty()) {
- d->formLayout->addRow(i18n("&From:"), createLabel(d->parser.from()));
+ d->formLayout->addRow(i18n("&From:"), new HeaderLabel(d->parser.from()));
}
if (!d->parser.sender().isEmpty() && d->parser.from() != d->parser.sender()) {
- d->formLayout->addRow(i18n("&Sender:"), createLabel(d->parser.sender()));
+ d->formLayout->addRow(i18n("&Sender:"), new HeaderLabel(d->parser.sender()));
}
if (!d->parser.to().isEmpty()) {
- d->formLayout->addRow(i18n("&To:"), createLabel(d->parser.to()));
+ d->formLayout->addRow(i18n("&To:"), new HeaderLabel(d->parser.to()));
}
if (!d->parser.cc().isEmpty()) {
- const auto label = new QLabel(d->parser.cc());
- label->setOpenExternalLinks(true);
- d->formLayout->addRow(i18n("&CC:"), createLabel(d->parser.cc()));
+ d->formLayout->addRow(i18n("&CC:"), new HeaderLabel(d->parser.cc()));
}
if (!d->parser.bcc().isEmpty()) {
- d->formLayout->addRow(i18n("&BCC:"), createLabel(d->parser.bcc()));
+ d->formLayout->addRow(i18n("&BCC:"), new HeaderLabel(d->parser.bcc()));
+ }
+ if (!d->parser.date().isNull()) {
+ d->formLayout->addRow(i18n("&Date:"), new HeaderLabel(QLocale::system().toString(d->parser.date().toLocalTime())));
}
const auto parts = d->parser.parts();
QLayoutItem *child;
while ((child = d->layout->takeAt(0)) != nullptr) {
delete child->widget();
delete child;
}
d->recursiveBuildViewer(parts, d->layout, {});
d->layout->addStretch();
d->attachmentView->setModel(d->parser.attachments());
d->attachmentView->setVisible(d->parser.attachments()->rowCount() > 0);
connect(d->attachmentView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [this] {
d->selectionChanged();
});
connect(d->attachmentView, &QAbstractItemView::doubleClicked, this, [this](const QModelIndex &) {
// Since this is only emitted if a valid index is double clicked we can assume
// that the first click of the double click set the selection accordingly.
d->openSelectedAttachments();
});
setUpdatesEnabled(true);
}
void MessageViewer::print(QPainter *painter, int width)
{
const auto oldSize = size();
resize(width - 30, oldSize.height());
d->scrollArea->setFrameShape(QFrame::NoFrame);
render(painter);
d->scrollArea->setFrameShape(QFrame::StyledPanel);
resize(oldSize);
}
diff --git a/src/widgets/messageviewerdialog.cpp b/src/widgets/messageviewerdialog.cpp
index 1397b83..3083c10 100644
--- a/src/widgets/messageviewerdialog.cpp
+++ b/src/widgets/messageviewerdialog.cpp
@@ -1,288 +1,291 @@
// SPDX-FileCopyrightText: 2023 g10 Code GmbH
// SPDX-FileContributor: Carl Schwan <carl.schwan@gnupg.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "messageviewerdialog.h"
#include "messageviewer.h"
#include "mimetreeparser_widgets_debug.h"
#include <MimeTreeParserCore/CryptoHelper>
#include <MimeTreeParserCore/FileOpener>
#include <KLocalizedString>
#include <KMessageBox>
#include <KMessageWidget>
#include <KMime/Message>
#include <QDialogButtonBox>
#include <QFileDialog>
#include <QMenuBar>
#include <QPainter>
#include <QPrintDialog>
#include <QPrintPreviewDialog>
#include <QPrinter>
#include <QPushButton>
#include <QRegularExpression>
#include <QSaveFile>
#include <QStandardPaths>
#include <QStyle>
#include <QToolBar>
#include <QVBoxLayout>
#include <memory>
using namespace MimeTreeParser::Widgets;
namespace
{
/// On windows, force the filename to end with .eml
/// On Linux, do nothing as this is handled by the file picker
inline QString changeExtension(const QString &fileName)
{
#ifdef Q_OS_WIN
auto renamedFileName = fileName;
renamedFileName.replace(QRegularExpression(QStringLiteral("\\.(mbox|p7m|asc)$")), QStringLiteral(".eml"));
// In case the file name didn't contain any of the expected extension: mbox, p7m, asc and eml
// or doesn't contains an extension at all.
if (!renamedFileName.endsWith(QStringLiteral(".eml"))) {
renamedFileName += QStringLiteral(".eml");
}
return renamedFileName;
#else
// Handled automatically by the file picker on linux
return fileName;
#endif
}
}
class MessageViewerDialog::Private
{
public:
int currentIndex = 0;
QVector<KMime::Message::Ptr> messages;
QString fileName;
MimeTreeParser::Widgets::MessageViewer *messageViewer = nullptr;
QAction *nextAction = nullptr;
QAction *previousAction = nullptr;
void setCurrentIndex(int currentIndex);
QMenuBar *createMenuBar(QWidget *parent);
private:
void save(QWidget *parent);
void saveDecrypted(QWidget *parent);
void print(QWidget *parent);
void printPreview(QWidget *parent);
void printInternal(QPrinter *printer);
};
void MessageViewerDialog::Private::setCurrentIndex(int index)
{
Q_ASSERT(index >= 0);
Q_ASSERT(index < messages.count());
currentIndex = index;
messageViewer->setMessage(messages[currentIndex]);
previousAction->setEnabled(currentIndex != 0);
nextAction->setEnabled(currentIndex != messages.count() - 1);
}
QMenuBar *MessageViewerDialog::Private::createMenuBar(QWidget *parent)
{
const auto menuBar = new QMenuBar(parent);
// File menu
const auto fileMenu = menuBar->addMenu(i18nc("@action:inmenu", "&File"));
const auto saveAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save")), i18nc("@action:inmenu", "&Save"));
QObject::connect(saveAction, &QAction::triggered, parent, [parent, this] {
save(parent);
});
fileMenu->addAction(saveAction);
const auto saveDecryptedAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save")), i18nc("@action:inmenu", "Save Decrypted"));
QObject::connect(saveDecryptedAction, &QAction::triggered, parent, [parent, this] {
saveDecrypted(parent);
});
fileMenu->addAction(saveDecryptedAction);
const auto printPreviewAction = new QAction(QIcon::fromTheme(QStringLiteral("document-print-preview")), i18nc("@action:inmenu", "Print Preview"));
QObject::connect(printPreviewAction, &QAction::triggered, parent, [parent, this] {
printPreview(parent);
});
fileMenu->addAction(printPreviewAction);
const auto printAction = new QAction(QIcon::fromTheme(QStringLiteral("document-print")), i18nc("@action:inmenu", "&Print"));
QObject::connect(printAction, &QAction::triggered, parent, [parent, this] {
print(parent);
});
fileMenu->addAction(printAction);
// Navigation menu
const auto navigationMenu = menuBar->addMenu(i18nc("@action:inmenu", "&Navigation"));
previousAction = new QAction(QIcon::fromTheme(QStringLiteral("go-previous")), i18nc("@action:button Previous email", "Previous Message"), parent);
previousAction->setEnabled(false);
navigationMenu->addAction(previousAction);
nextAction = new QAction(QIcon::fromTheme(QStringLiteral("go-next")), i18nc("@action:button Next email", "Next Message"), parent);
nextAction->setEnabled(false);
navigationMenu->addAction(nextAction);
return menuBar;
}
void MessageViewerDialog::Private::save(QWidget *parent)
{
const QString location = QFileDialog::getSaveFileName(parent,
i18nc("@title:window", "Save File"),
changeExtension(fileName),
i18nc("File dialog accepted files", "Email files (*.eml *.mbox)"));
QSaveFile file(location);
if (!file.open(QIODevice::WriteOnly)) {
KMessageBox::error(parent, i18n("File %1 could not be created.", location), i18n("Error saving message"));
return;
}
file.write(messages[currentIndex]->encodedContent());
file.commit();
}
void MessageViewerDialog::Private::saveDecrypted(QWidget *parent)
{
const QString location = QFileDialog::getSaveFileName(parent,
i18nc("@title:window", "Save Decrypted File"),
changeExtension(fileName),
i18nc("File dialog accepted files", "Email files (*.eml *.mbox)"));
QSaveFile file(location);
if (!file.open(QIODevice::WriteOnly)) {
KMessageBox::error(parent, i18nc("Error message", "File %1 could not be created.", location), i18n("Error saving message"));
return;
}
auto message = messages[currentIndex];
bool wasEncrypted = false;
auto decryptedMessage = CryptoUtils::decryptMessage(message, wasEncrypted);
if (!wasEncrypted) {
decryptedMessage = message;
}
file.write(decryptedMessage->encodedContent());
file.commit();
}
void MessageViewerDialog::Private::print(QWidget *parent)
{
QPrinter printer;
QPrintDialog dialog(&printer, parent);
dialog.setWindowTitle(i18nc("@title:window", "Print Document"));
if (dialog.exec() != QDialog::Accepted)
return;
printInternal(&printer);
}
void MessageViewerDialog::Private::printPreview(QWidget *parent)
{
auto dialog = new QPrintPreviewDialog(parent);
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->resize(800, 750);
dialog->setWindowTitle(i18nc("@title:window", "Print Document"));
QObject::connect(dialog, &QPrintPreviewDialog::paintRequested, parent, [this](QPrinter *printer) {
printInternal(printer);
});
dialog->open();
}
void MessageViewerDialog::Private::printInternal(QPrinter *printer)
{
QPainter painter;
painter.begin(printer);
const auto pageLayout = printer->pageLayout();
const auto pageRect = pageLayout.paintRectPixels(printer->resolution());
const double xscale = pageRect.width() / double(messageViewer->width());
const double yscale = pageRect.height() / double(messageViewer->height());
const double scale = qMin(qMin(xscale, yscale), 1.);
painter.translate(pageRect.x(), pageRect.y());
painter.scale(scale, scale);
messageViewer->print(&painter, pageRect.width());
}
MessageViewerDialog::MessageViewerDialog(const QString &fileName, QWidget *parent)
: QDialog(parent)
, d(std::make_unique<Private>())
{
d->fileName = fileName;
const auto mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins({});
const auto layout = new QVBoxLayout;
layout->setContentsMargins(style()->pixelMetric(QStyle::PM_LayoutLeftMargin, nullptr, this),
style()->pixelMetric(QStyle::PM_LayoutTopMargin, nullptr, this),
style()->pixelMetric(QStyle::PM_LayoutRightMargin, nullptr, this),
style()->pixelMetric(QStyle::PM_LayoutBottomMargin, nullptr, this));
const auto menuBar = d->createMenuBar(this);
mainLayout->setMenuBar(menuBar);
d->messages += MimeTreeParser::Core::FileOpener::openFile(fileName);
if (d->messages.isEmpty()) {
auto errorMessage = new KMessageWidget(this);
errorMessage->setMessageType(KMessageWidget::Error);
errorMessage->setText(i18nc("@info", "Unable to read file"));
layout->addWidget(errorMessage);
return;
}
const bool multipleMessages = d->messages.length() > 1;
if (multipleMessages) {
const auto toolBar = new QToolBar(this);
#ifdef Q_OS_UNIX
toolBar->setToolButtonStyle(Qt::ToolButtonFollowStyle);
#else
// on other platforms the default is IconOnly which is bad for
// accessibility and can't be changed by the user.
toolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
#endif
toolBar->addAction(d->previousAction);
connect(d->previousAction, &QAction::triggered, this, [this] {
d->setCurrentIndex(d->currentIndex - 1);
});
const auto spacer = new QWidget(this);
spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
toolBar->addWidget(spacer);
toolBar->addAction(d->nextAction);
connect(d->nextAction, &QAction::triggered, this, [this] {
d->setCurrentIndex(d->currentIndex + 1);
});
d->nextAction->setEnabled(true);
mainLayout->addWidget(toolBar);
}
mainLayout->addLayout(layout);
d->messageViewer = new MimeTreeParser::Widgets::MessageViewer(this);
d->messageViewer->setMessage(d->messages[0]);
layout->addWidget(d->messageViewer);
auto buttonBox = new QDialogButtonBox(this);
auto closeButton = buttonBox->addButton(QDialogButtonBox::Close);
connect(closeButton, &QPushButton::pressed, this, &QDialog::accept);
layout->addWidget(buttonBox);
+
+ setMinimumSize(300, 300);
+ resize(600, 600);
}
MessageViewerDialog::~MessageViewerDialog() = default;
QVector<KMime::Message::Ptr> MessageViewerDialog::messages() const
{
return d->messages;
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Thu, Jul 17, 12:51 AM (1 d, 13 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
e4/20/76b69ce40483d31bb153c54bb6c3
Attached To
rMTP MIME Tree Parser
Event Timeline
Log In to Comment