Page MenuHome GnuPG

No OneTemporary

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8ba1011..569d1f6 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,148 +1,148 @@
# SPDX-FileCopyrightText: 2023 Carl Schwan <carl.schwan@gnupg.com>
# SPDX-License-Identifier: BSD-3-Clause
cmake_minimum_required(VERSION 3.16 FATAL_ERROR)
set(PIM_VERSION "6.5.40")
project(MimeTreeParserNG VERSION ${PIM_VERSION})
# ECM setup
set(KF_MIN_VERSION "6.17.0")
find_package(ECM ${KF_MIN_VERSION} CONFIG REQUIRED)
set(CMAKE_MODULE_PATH
${CMAKE_MODULE_PATH}
${ECM_MODULE_PATH}
${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules
)
set(QT_REQUIRED_VERSION "6.7.0")
set(CMAKE_CXX_SCAN_FOR_MODULES OFF)
include(KDEInstallDirs)
include(KDECMakeSettings)
include(KDECompilerSettings NO_POLICY_SCOPE)
include(GenerateExportHeader)
include(ECMGenerateHeaders)
include(ECMGeneratePriFile)
include(ECMQmlModule)
include(ECMSetupVersion)
include(KDEGitCommitHooks)
include(KDEClangFormat)
file(
GLOB_RECURSE ALL_CLANG_FORMAT_SOURCE_FILES
autotests/*.cpp
autotests/*.h
src/*.cpp
src/*.h
examples/*.cpp
examples/*.h
)
kde_clang_format(${ALL_CLANG_FORMAT_SOURCE_FILES})
include(ECMQtDeclareLoggingCategory)
include(ECMDeprecationSettings)
include(ECMFeatureSummary)
include(ECMAddQch)
include(ECMAddTests)
include(ECMCheckOutboundLicense)
file(
GLOB_RECURSE ALL_SOURCE_FILES
autotests/*.cpp
autotests/*.h
src/*.cpp
src/*.h
examples/*.cpp
examples/*.h
)
ecm_check_outbound_license(LICENSES GPL-2.0-only FILES ${ALL_SOURCE_FILES})
-set(KPIM_MIME_VERSION "6.5.40")
+set(KPIM_MIME_VERSION "6.5.42")
set(KPIM_LIBKLEO_VERSION "6.5.40")
set(KPIM_MBOX_VERSION "6.5.40")
set(GPGME_REQUIRED_VERSION "1.23.2")
ecm_setup_version(PROJECT
VARIABLE_PREFIX MIMETREEPARSERNG
VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/mimetreeparserng_version.h"
PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KPim6MimeTreeParserCoreConfigVersion.cmake"
SOVERSION 6
)
configure_file(
mimetreeparserng-version.h.in
${CMAKE_CURRENT_BINARY_DIR}/src/mimetreeparserng-version.h
@ONLY
)
option(
BUILD_QCH
"Build API documentation in QCH format (for e.g. Qt Assistant, Qt Creator & KDevelop)"
OFF
)
add_feature_info(
QCH
${BUILD_QCH}
"API documentation in QCH format (for e.g. Qt Assistant, Qt Creator & KDevelop)"
)
if(BUILD_TESTING)
add_definitions(-DBUILD_TESTING)
endif()
########### Find packages ###########
find_package(Qt6Gui ${QT_REQUIRED_VERSION} CONFIG REQUIRED)
find_package(Qt6PrintSupport ${QT_REQUIRED_VERSION} CONFIG REQUIRED)
find_package(KF6I18n ${KF_MIN_VERSION} CONFIG REQUIRED)
find_package(KF6CalendarCore ${KF_MIN_VERSION} CONFIG REQUIRED)
find_package(KF6ColorScheme ${KF_MIN_VERSION} CONFIG REQUIRED)
find_package(KF6WidgetsAddons ${KF_MIN_VERSION} CONFIG)
find_package(KPim6Mime ${KPIM_MIME_VERSION} CONFIG REQUIRED)
find_package(KPim6Mbox ${KPIM_MBOX_VERSION} CONFIG REQUIRED)
find_package(KPim6Libkleo ${KPIM_LIBKLEO_VERSION} CONFIG REQUIRED)
find_package(Gpgmepp ${GPGME_REQUIRED_VERSION} CONFIG REQUIRED)
find_package(Qt6Quick ${QT_REQUIRED_VERSION} CONFIG)
find_package(Qt6Widgets ${QT_REQUIRED_VERSION} CONFIG)
if(BUILD_TESTING)
find_package(Qt6Test ${QT_REQUIRED_VERSION} CONFIG REQUIRED)
endif()
########### Targets ###########
add_definitions(-DQT_NO_CONTEXTLESS_CONNECT)
ecm_set_disabled_deprecation_versions(QT 6.10.0 KF 6.18.0)
option(
USE_UNITY_CMAKE_SUPPORT
"Use UNITY cmake support (speedup compile time)"
OFF
)
set(COMPILE_WITH_UNITY_CMAKE_SUPPORT OFF)
if(USE_UNITY_CMAKE_SUPPORT)
set(COMPILE_WITH_UNITY_CMAKE_SUPPORT ON)
add_definitions(-DCOMPILE_WITH_UNITY_CMAKE_SUPPORT)
endif()
add_subdirectory(src)
if(BUILD_TESTING)
add_subdirectory(examples)
add_subdirectory(autotests)
endif()
install(
FILES ${CMAKE_CURRENT_BINARY_DIR}/mimetreeparserng_version.h
DESTINATION ${KDE_INSTALL_INCLUDEDIR}/KPim6/MimeTreeParserCore
COMPONENT Devel
)
ecm_qt_install_logging_categories(
EXPORT MIMETREEPARSERNG
FILE mimetreeparser.categories
DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR}
)
kde_configure_git_pre_commit_hook(CHECKS CLANG_FORMAT)
ki18n_install(po)
ecm_feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES)
diff --git a/autotests/core/cryptohelpertest.cpp b/autotests/core/cryptohelpertest.cpp
index 668c00c..b6ef859 100644
--- a/autotests/core/cryptohelpertest.cpp
+++ b/autotests/core/cryptohelpertest.cpp
@@ -1,183 +1,183 @@
// SPDX-FileCopyrightText: 2015 Sandro Knauß <knauss@kolabsys.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include "cryptohelpertest.h"
#include "cryptohelper.h"
#include <QTest>
using namespace MimeTreeParser;
static QByteArray readMailFromFile(const QString &mailFile)
{
QFile file(QLatin1StringView(MAIL_DATA_DIR) + QLatin1Char('/') + mailFile);
file.open(QIODevice::ReadOnly);
Q_ASSERT(file.isOpen());
return file.readAll();
}
void CryptoHelperTest::testPMFDEmpty()
{
QCOMPARE(prepareMessageForDecryption("").count(), 0);
}
void CryptoHelperTest::testPMFDWithNoPGPBlock()
{
const QByteArray text = "testblabla";
const QList<Block> blocks = prepareMessageForDecryption(text);
QCOMPARE(blocks.count(), 1);
QCOMPARE(blocks[0].text(), text);
QCOMPARE(blocks[0].type(), NoPgpBlock);
}
void CryptoHelperTest::testPGPBlockType()
{
const QString blockText = QStringLiteral("text");
const QString preString = QStringLiteral("before\n");
for (int i = 1; i <= PrivateKeyBlock; ++i) {
QString name;
switch (i) {
case PgpMessageBlock:
name = QStringLiteral("MESSAGE");
break;
case MultiPgpMessageBlock:
name = QStringLiteral("MESSAGE PART");
break;
case SignatureBlock:
name = QStringLiteral("SIGNATURE");
break;
case ClearsignedBlock:
name = QStringLiteral("SIGNED MESSAGE");
break;
case PublicKeyBlock:
name = QStringLiteral("PUBLIC KEY BLOCK");
break;
case PrivateKeyBlock:
name = QStringLiteral("PRIVATE KEY BLOCK");
break;
}
QString text = QLatin1StringView("-----BEGIN PGP ") + name + QLatin1Char('\n') + blockText;
QList<Block> blocks = prepareMessageForDecryption(preString.toLatin1() + text.toLatin1());
QCOMPARE(blocks.count(), 1);
QCOMPARE(blocks[0].type(), UnknownBlock);
text += QLatin1StringView("\n-----END PGP ") + name + QLatin1Char('\n');
blocks = prepareMessageForDecryption(preString.toLatin1() + text.toLatin1());
QCOMPARE(blocks.count(), 2);
QCOMPARE(blocks[1].text(), text.toLatin1());
QCOMPARE(blocks[1].type(), static_cast<PGPBlockType>(i));
}
}
void CryptoHelperTest::testDeterminePGPBlockType()
{
const QString blockText = QStringLiteral("text");
for (int i = 1; i <= PrivateKeyBlock; ++i) {
QString name;
switch (i) {
case PgpMessageBlock:
name = QStringLiteral("MESSAGE");
break;
case MultiPgpMessageBlock:
name = QStringLiteral("MESSAGE PART");
break;
case SignatureBlock:
name = QStringLiteral("SIGNATURE");
break;
case ClearsignedBlock:
name = QStringLiteral("SIGNED MESSAGE");
break;
case PublicKeyBlock:
name = QStringLiteral("PUBLIC KEY BLOCK");
break;
case PrivateKeyBlock:
name = QStringLiteral("PRIVATE KEY BLOCK");
break;
}
const QString text = QLatin1StringView("-----BEGIN PGP ") + name + QLatin1Char('\n') + blockText + QLatin1Char('\n');
const Block block = Block(text.toLatin1());
QCOMPARE(block.text(), text.toLatin1());
QCOMPARE(block.type(), static_cast<PGPBlockType>(i));
}
}
void CryptoHelperTest::testEmbededPGPBlock()
{
const QByteArray text = QByteArray("before\n-----BEGIN PGP MESSAGE-----\ncrypted - you see :)\n-----END PGP MESSAGE-----\nafter");
const QList<Block> blocks = prepareMessageForDecryption(text);
QCOMPARE(blocks.count(), 3);
QCOMPARE(blocks[0].text(), QByteArray("before\n"));
QCOMPARE(blocks[1].text(), QByteArray("-----BEGIN PGP MESSAGE-----\ncrypted - you see :)\n-----END PGP MESSAGE-----\n"));
QCOMPARE(blocks[2].text(), QByteArray("after"));
}
void CryptoHelperTest::testClearSignedMessage()
{
const QByteArray text = QByteArray(
"before\n-----BEGIN PGP SIGNED MESSAGE-----\nsigned content\n-----BEGIN PGP SIGNATURE-----\nfancy signature\n-----END PGP SIGNATURE-----\nafter");
const QList<Block> blocks = prepareMessageForDecryption(text);
QCOMPARE(blocks.count(), 3);
QCOMPARE(blocks[0].text(), QByteArray("before\n"));
QCOMPARE(blocks[1].text(),
QByteArray("-----BEGIN PGP SIGNED MESSAGE-----\nsigned content\n-----BEGIN PGP SIGNATURE-----\nfancy signature\n-----END PGP SIGNATURE-----\n"));
QCOMPARE(blocks[2].text(), QByteArray("after"));
}
void CryptoHelperTest::testMultipleBlockMessage()
{
const QByteArray text = QByteArray(
"before\n-----BEGIN PGP SIGNED MESSAGE-----\nsigned content\n-----BEGIN PGP SIGNATURE-----\nfancy signature\n-----END PGP "
"SIGNATURE-----\nafter\n-----BEGIN PGP MESSAGE-----\ncrypted - you see :)\n-----END PGP MESSAGE-----\n");
const QList<Block> blocks = prepareMessageForDecryption(text);
QCOMPARE(blocks.count(), 4);
QCOMPARE(blocks[0].text(), QByteArray("before\n"));
QCOMPARE(blocks[1].text(),
QByteArray("-----BEGIN PGP SIGNED MESSAGE-----\nsigned content\n-----BEGIN PGP SIGNATURE-----\nfancy signature\n-----END PGP SIGNATURE-----\n"));
QCOMPARE(blocks[2].text(), QByteArray("after\n"));
QCOMPARE(blocks[3].text(), QByteArray("-----BEGIN PGP MESSAGE-----\ncrypted - you see :)\n-----END PGP MESSAGE-----\n"));
}
void CryptoHelperTest::testDecryptMessage()
{
auto message = KMime::Message::Ptr(new KMime::Message);
message->setContent(readMailFromFile(QLatin1StringView("openpgp-encrypted+signed.mbox")));
message->parse();
bool wasEncrypted = false;
GpgME::Protocol protocol;
auto decryptedMessage = CryptoUtils::decryptMessage(message, wasEncrypted, protocol);
QVERIFY(wasEncrypted);
QVERIFY(decryptedMessage);
- QCOMPARE(decryptedMessage->decodedContent(), QByteArray("encrypted message text"));
+ QCOMPARE(decryptedMessage->decodedBody(), QByteArray("encrypted message text"));
QCOMPARE(decryptedMessage->encodedContent(),
QByteArray("From test@kolab.org Wed, 08 Sep 2010 17: 02:52 +0200\nFrom: OpenPGP Test <test@kolab.org>\nTo: test@kolab.org\nSubject: OpenPGP "
"encrypted\nDate: Wed, 08 Sep 2010 17:02:52 +0200\nUser-Agent: KMail/4.6 pre (Linux/2.6.34-rc2-2-default; KDE/4.5.60; x86_64; ; "
")\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7Bit\nContent-Type: text/plain; charset=\"us-ascii\"\n\nencrypted message text"));
QCOMPARE(protocol, GpgME::OpenPGP);
}
void CryptoHelperTest::testDecryptInlineMessage()
{
auto message = KMime::Message::Ptr(new KMime::Message);
message->setContent(readMailFromFile(QLatin1StringView("openpgp-inline-encrypted+nonenc.mbox")));
message->parse();
bool wasEncrypted = false;
GpgME::Protocol protocol;
auto decryptedMessage = CryptoUtils::decryptMessage(message, wasEncrypted, protocol);
QVERIFY(wasEncrypted);
QVERIFY(decryptedMessage);
- QCOMPARE(decryptedMessage->decodedContent(), QByteArray("Not encrypted not signed :(\n\nsome random text\n"));
+ QCOMPARE(decryptedMessage->decodedBody(), QByteArray("Not encrypted not signed :(\n\nsome random text\n"));
QCOMPARE(decryptedMessage->encodedContent(),
QByteArray("From test@kolab.org Wed, 25 May 2011 23: 49:40 +0100\nFrom: OpenPGP Test <test@kolab.org>\nTo: test@kolab.org\nSubject: "
"inlinepgpencrypted + non enc text\nDate: Wed, 25 May 2011 23:49:40 +0100\nMessage-ID: "
"<1786696.yKXrOjjflF@herrwackelpudding.localhost>\nX-KMail-Transport: GMX\nX-KMail-Fcc: 28\nX-KMail-Drafts: 7\nX-KMail-Templates: "
"9\nUser-Agent: KMail/4.6 beta5 (Linux/2.6.34.7-0.7-desktop; KDE/4.6.41; x86_64;\n git-0269848; 2011-04-19)\nMIME-Version: "
"1.0\nContent-Type: text/plain; charset=\"us-ascii\"\n\nNot encrypted not signed :(\n\nsome random text\n"));
QCOMPARE(protocol, GpgME::OpenPGP);
}
QTEST_APPLESS_MAIN(CryptoHelperTest)
#include "moc_cryptohelpertest.cpp"
diff --git a/src/core/attachmentmodel.cpp b/src/core/attachmentmodel.cpp
index 6cb6003..0475d37 100644
--- a/src/core/attachmentmodel.cpp
+++ b/src/core/attachmentmodel.cpp
@@ -1,417 +1,417 @@
// SPDX-FileCopyrightText: 2016 Sandro Knauß <knauss@kolabsys.com>
// SPDX-FileCopyCopyright: 2017 Christian Mollekopf <mollekopf@kolabsys.com>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "attachmentmodel.h"
using namespace Qt::Literals::StringLiterals;
#include "mimetreeparser_core_debug.h"
#include <QGpgME/ImportJob>
#include <QGpgME/Protocol>
#include <KLocalizedString>
#include <KMime/Content>
#include <QDesktopServices>
#include <QDir>
#include <QFile>
#include <QGuiApplication>
#include <QIcon>
#include <QMimeDatabase>
#include <QMimeType>
#include <QRegularExpression>
#include <QStandardPaths>
#include <QTemporaryDir>
#include <QUrl>
#ifdef Q_OS_WIN
#include <cstdio>
#include <string>
#include <vector>
#include <windows.h>
#endif
namespace
{
QString sizeHuman(float size)
{
QStringList list;
list << u"KB"_s << u"MB"_s << QStringLiteral("GB") << QStringLiteral("TB");
QStringListIterator i(list);
QString unit = u"Bytes"_s;
while (size >= 1024.0 && i.hasNext()) {
unit = i.next();
size /= 1024.0;
}
if (unit == QLatin1StringView("Bytes")) {
return QString().setNum(size) + u" "_s + unit;
} else {
return QString().setNum(size, 'f', 2) + u" "_s + unit;
}
}
// SPDX-SnippetBegin
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: GPL-3.0-only
#define WINDOWS_DEVICES_PATTERN "(CON|AUX|PRN|NUL|COM[1-9]|LPT[1-9])(\\..*)?"
// Naming a file like a device name will break on Windows, even if it is
// "com1.txt". Since we are cross-platform, we generally disallow such file
// names.
const QRegularExpression &windowsDeviceNoSubDirPattern()
{
static const QRegularExpression rc(u"^" WINDOWS_DEVICES_PATTERN "$"_s, QRegularExpression::CaseInsensitiveOption);
Q_ASSERT(rc.isValid());
return rc;
}
const QRegularExpression &windowsDeviceSubDirPattern()
{
static const QRegularExpression rc(u"^.*[/\\\\]" WINDOWS_DEVICES_PATTERN "$"_s, QRegularExpression::CaseInsensitiveOption);
Q_ASSERT(rc.isValid());
return rc;
}
/* Validate a file base name, check for forbidden characters/strings. */
#define SLASHES "/\\"
static const char notAllowedCharsSubDir[] = ",^@={}[]~!?:&*\"|#%<>$\"'();`' ";
static const char notAllowedCharsNoSubDir[] = ",^@={}[]~!?:&*\"|#%<>$\"'();`' " SLASHES;
static const char *notAllowedSubStrings[] = {".."};
bool validateFileName(const QString &name, bool allowDirectories)
{
if (name.isEmpty()) {
return false;
}
// Characters
const char *notAllowedChars = allowDirectories ? notAllowedCharsSubDir : notAllowedCharsNoSubDir;
for (const char *c = notAllowedChars; *c; c++) {
if (name.contains(QLatin1Char(*c))) {
return false;
}
}
// Substrings
const int notAllowedSubStringCount = sizeof(notAllowedSubStrings) / sizeof(const char *);
for (int s = 0; s < notAllowedSubStringCount; s++) {
const QLatin1StringView notAllowedSubString(notAllowedSubStrings[s]);
if (name.contains(notAllowedSubString)) {
return false;
}
}
// Windows devices
bool matchesWinDevice = name.contains(windowsDeviceNoSubDirPattern());
if (!matchesWinDevice && allowDirectories) {
matchesWinDevice = name.contains(windowsDeviceSubDirPattern());
}
return !matchesWinDevice;
}
// SPDX-SnippetEnd
}
#ifdef Q_OS_WIN
struct WindowFile {
std::wstring fileName;
std::wstring dirName;
HANDLE handle;
};
#endif
class AttachmentModelPrivate
{
public:
AttachmentModelPrivate(AttachmentModel *q_ptr, const std::shared_ptr<MimeTreeParser::ObjectTreeParser> &parser);
AttachmentModel *const q;
QMimeDatabase mimeDb;
std::shared_ptr<MimeTreeParser::ObjectTreeParser> mParser;
MimeTreeParser::MessagePart::List mAttachments;
#ifdef Q_OS_WIN
std::vector<WindowFile> mOpenFiles;
#endif
};
AttachmentModelPrivate::AttachmentModelPrivate(AttachmentModel *q_ptr, const std::shared_ptr<MimeTreeParser::ObjectTreeParser> &parser)
: q(q_ptr)
, mParser(parser)
{
mAttachments = mParser->collectAttachmentParts();
}
AttachmentModel::AttachmentModel(std::shared_ptr<MimeTreeParser::ObjectTreeParser> parser)
: QAbstractTableModel()
, d(std::unique_ptr<AttachmentModelPrivate>(new AttachmentModelPrivate(this, parser)))
{
}
AttachmentModel::~AttachmentModel()
{
#ifdef Q_OS_WIN
for (const auto &file : d->mOpenFiles) {
// As owner of the file we need to close our handle first
// With FILE_SHARE_DELETE we have ensured that all _other_ processes must
// have opened the file with FILE_SHARE_DELETE, too.
if (!CloseHandle(file.handle)) {
// Always get the last error before calling any Qt functions that may
// use Windows system calls.
DWORD err = GetLastError();
qWarning() << "Unable to close handle for file" << QString::fromStdWString(file.fileName) << err;
}
if (!DeleteFileW(file.fileName.c_str())) {
DWORD err = GetLastError();
qWarning() << "Unable to delete file" << QString::fromStdWString(file.fileName) << err;
}
if (!RemoveDirectoryW(file.dirName.c_str())) {
DWORD err = GetLastError();
qWarning() << "Unable to delete temporary directory" << QString::fromStdWString(file.dirName) << err;
}
}
#endif
}
QHash<int, QByteArray> AttachmentModel::roleNames() const
{
return {
{TypeRole, "type"_ba},
{NameRole, "name"_ba},
{SizeRole, "size"_ba},
{IconRole, "iconName"_ba},
{IsEncryptedRole, "encrypted"_ba},
{IsSignedRole, "signed"_ba},
};
}
QVariant AttachmentModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
switch (section) {
case NameColumn:
return i18ndc("mimetreeparser", "@title:column", "Name");
case SizeColumn:
return i18ndc("mimetreeparser", "@title:column", "Size");
case IsEncryptedColumn:
return i18ndc("mimetreeparser", "@title:column", "Encrypted");
case IsSignedColumn:
return i18ndc("mimetreeparser", "@title:column", "Signed");
}
}
return {};
}
QVariant AttachmentModel::data(const QModelIndex &index, int role) const
{
const auto row = index.row();
const auto column = index.column();
const auto part = d->mAttachments.at(row);
Q_ASSERT(part);
auto node = part->node();
if (!node) {
qWarning() << "no content for attachment";
return {};
}
const auto mimetype = d->mimeDb.mimeTypeForName(QString::fromLatin1(part->mimeType()));
const auto content = node->encodedContent();
switch (column) {
case NameColumn:
switch (role) {
case TypeRole:
return mimetype.name();
case Qt::DisplayRole:
case NameRole:
return part->filename();
case IconRole:
return mimetype.iconName();
case Qt::DecorationRole:
return QIcon::fromTheme(mimetype.iconName());
case SizeRole:
return sizeHuman(content.size());
case IsEncryptedRole:
return part->encryptions().size() > 0;
case IsSignedRole:
return part->signatures().size() > 0;
case AttachmentPartRole:
return QVariant::fromValue(part);
default:
return {};
}
case SizeColumn:
switch (role) {
case Qt::DisplayRole:
return sizeHuman(content.size());
default:
return {};
}
case IsEncryptedColumn:
switch (role) {
case Qt::CheckStateRole:
return part->encryptions().size() > 0 ? Qt::Checked : Qt::Unchecked;
default:
return {};
}
case IsSignedColumn:
switch (role) {
case Qt::CheckStateRole:
return part->signatures().size() > 0 ? Qt::Checked : Qt::Unchecked;
default:
return {};
}
default:
return {};
}
}
QString AttachmentModel::saveAttachmentToPath(const int row, const QString &path)
{
const auto part = d->mAttachments.at(row);
return saveAttachmentToPath(part, path);
}
QString AttachmentModel::saveAttachmentToPath(const MimeTreeParser::MessagePart::Ptr &part, const QString &path)
{
Q_ASSERT(part);
auto node = part->node();
- auto data = node->decodedContent();
+ auto data = node->decodedBody();
// This is necessary to store messages embedded messages (EncapsulatedRfc822MessagePart)
if (data.isEmpty()) {
data = node->encodedContent();
}
if (part->isText()) {
// convert CRLF to LF before writing text attachments to disk
data = KMime::CRLFtoLF(data);
}
QFile f(path);
if (!f.open(QIODevice::WriteOnly)) {
qCWarning(MIMETREEPARSER_CORE_LOG) << "Failed to write attachment to file:" << path << " Error: " << f.errorString();
Q_EMIT errorOccurred(i18ndc("mimetreeparser", "@info", "Failed to save attachment."));
return {};
}
f.write(data);
f.close();
qCInfo(MIMETREEPARSER_CORE_LOG) << "Wrote attachment to file: " << path;
return path;
}
bool AttachmentModel::openAttachment(const int row)
{
const auto part = d->mAttachments.at(row);
return openAttachment(part);
}
bool AttachmentModel::openAttachment(const MimeTreeParser::MessagePart::Ptr &message)
{
QString fileName = message->filename();
QTemporaryDir tempDir(QDir::tempPath() + QLatin1Char('/') + qGuiApp->applicationName() + u".XXXXXX"_s);
// TODO: We need some cleanup here. Otherwise the files will stay forever on Windows.
tempDir.setAutoRemove(false);
if (message->filename().isEmpty() || !validateFileName(fileName, false)) {
const auto mimetype = d->mimeDb.mimeTypeForName(QString::fromLatin1(message->mimeType()));
fileName = tempDir.filePath(i18n("attachment") + QLatin1Char('.') + mimetype.preferredSuffix());
} else {
fileName = tempDir.filePath(message->filename());
}
const auto filePath = saveAttachmentToPath(message, fileName);
if (filePath.isEmpty()) {
Q_EMIT errorOccurred(i18ndc("mimetreeparser", "@info", "Failed to write attachment for opening."));
return false;
}
#ifdef Q_OS_WIN
std::wstring fileNameStr = filePath.toStdWString();
HANDLE hFile = CreateFileW(fileNameStr.c_str(),
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_DELETE, // allow other processes to delete it
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, // Using FILE_FLAG_DELETE_ON_CLOSE causes some
// applications like windows zip not to open the
// file.
NULL // no template
);
if (hFile == INVALID_HANDLE_VALUE) {
Q_EMIT errorOccurred(i18ndc("mimetreeparser", "@info", "Failed to open attachment."));
QFile file(fileName);
file.remove();
return false;
}
d->mOpenFiles.push_back({fileNameStr, tempDir.path().toStdWString(), hFile});
#endif
if (!QDesktopServices::openUrl(QUrl::fromLocalFile(filePath))) {
Q_EMIT errorOccurred(i18ndc("mimetreeparser", "@info", "Failed to open attachment."));
return false;
}
return true;
}
bool AttachmentModel::importPublicKey(const int row)
{
const auto part = d->mAttachments.at(row);
return importPublicKey(part);
}
bool AttachmentModel::importPublicKey(const MimeTreeParser::MessagePart::Ptr &part)
{
Q_ASSERT(part);
- const QByteArray certData = part->node()->decodedContent();
+ const QByteArray certData = part->node()->decodedBody();
QGpgME::ImportJob *importJob = QGpgME::openpgp()->importJob();
connect(importJob, &QGpgME::AbstractImportJob::result, this, [this](const GpgME::ImportResult &result) {
if (result.numConsidered() == 0) {
Q_EMIT errorOccurred(i18ndc("mimetreeparser", "@info", "No certificates were found in this attachment"));
return;
} else {
QString message = i18ndcp("mimetreeparser", "@info", "one certificate imported", "%1 certificates imported", result.numImported());
if (result.numUnchanged() != 0) {
message += u"\n"_s
+ i18ndcp("mimetreeparser",
"@info",
"one certificate was already imported",
"%1 certificates were already imported",
result.numUnchanged());
}
Q_EMIT info(message);
}
});
GpgME::Error err = importJob->start(certData);
return !err;
}
int AttachmentModel::rowCount(const QModelIndex &parent) const
{
if (!parent.isValid()) {
return d->mAttachments.size();
}
return 0;
}
int AttachmentModel::columnCount(const QModelIndex &parent) const
{
if (!parent.isValid()) {
return ColumnCount;
}
return 0;
}
#include "moc_attachmentmodel.cpp"
diff --git a/src/core/bodypartformatter_impl.cpp b/src/core/bodypartformatter_impl.cpp
index 4ff71c2..4c5f7bd 100644
--- a/src/core/bodypartformatter_impl.cpp
+++ b/src/core/bodypartformatter_impl.cpp
@@ -1,392 +1,392 @@
// SPDX-FileCopyrightText: 2003 Marc Mutz <mutz@kde.org>
// SPDX-License-Identifier: GPL-2.0-only
#include "mimetreeparser_core_debug.h"
#include "bodypartformatter.h"
#include "bodypartformatterbasefactory.h"
#include "bodypartformatterbasefactory_p.h"
#include "messagepart.h"
#include "objecttreeparser.h"
#include "utils.h"
#include <KMime/Content>
#include <QGpgME/Protocol>
using namespace MimeTreeParser;
using namespace MimeTreeParser::Interface;
using namespace Qt::Literals::StringLiterals;
namespace MimeTreeParser
{
class AnyTypeBodyPartFormatter : public MimeTreeParser::Interface::BodyPartFormatter
{
};
class MessageRfc822BodyPartFormatter : public MimeTreeParser::Interface::BodyPartFormatter
{
public:
MessagePart::Ptr process(ObjectTreeParser *objectTreeParser, KMime::Content *node) const override
{
return MessagePart::Ptr(new EncapsulatedRfc822MessagePart(objectTreeParser, node, node->bodyAsMessage()));
}
};
class HeadersBodyPartFormatter : public MimeTreeParser::Interface::BodyPartFormatter
{
public:
MessagePart::Ptr process(ObjectTreeParser *objectTreeParser, KMime::Content *node) const override
{
return MessagePart::Ptr(new HeadersPart(objectTreeParser, node));
}
};
class MultiPartRelatedBodyPartFormatter : public MimeTreeParser::Interface::BodyPartFormatter
{
public:
QList<MessagePart::Ptr> processList(ObjectTreeParser *objectTreeParser, KMime::Content *node) const override
{
if (node->contents().isEmpty()) {
return {};
}
// We rely on the order of the parts.
// Theoretically there could also be a Start parameter which would break this..
// https://tools.ietf.org/html/rfc2387#section-4
// We want to display attachments even if displayed inline.
QList<MessagePart::Ptr> list;
list.append(MimeMessagePart::Ptr(new MimeMessagePart(objectTreeParser, node->contents().at(0), true)));
for (int i = 1; i < node->contents().size(); i++) {
auto p = node->contents().at(i);
if (KMime::isAttachment(p)) {
list.append(MimeMessagePart::Ptr(new MimeMessagePart(objectTreeParser, p, true)));
}
}
return list;
}
};
class MultiPartMixedBodyPartFormatter : public MimeTreeParser::Interface::BodyPartFormatter
{
public:
MessagePart::Ptr process(ObjectTreeParser *objectTreeParser, KMime::Content *node) const override
{
auto contents = node->contents();
if (contents.isEmpty()) {
return {};
}
bool isActuallyMixedEncrypted = false;
for (const auto &content : std::as_const(contents)) {
if (content->contentType()->mimeType() == "application/pgp-encrypted"_ba || content->contentType()->mimeType() == "application/pkcs7-mime"_ba) {
isActuallyMixedEncrypted = true;
}
}
if (isActuallyMixedEncrypted) {
// Remove explaination
contents.erase(std::remove_if(contents.begin(),
contents.end(),
[](const auto content) {
return content->contentType()->mimeType() == "text/plain";
}),
contents.end());
if (contents.count() == 1 && contents[0]->contentType()->mimeType() == "application/pkcs7-mime"_ba) {
auto data = findTypeInDirectChildren(node, "application/pkcs7-mime"_ba);
auto mp = EncryptedMessagePart::Ptr(new EncryptedMessagePart(objectTreeParser, data->decodedText(), QGpgME::smime(), node, data));
mp->setIsEncrypted(true);
return mp;
}
if (contents.count() == 2 && contents[1]->contentType()->mimeType() == "application/octet-stream"_ba) {
KMime::Content *data = findTypeInDirectChildren(node, "application/octet-stream"_ba);
auto mp = EncryptedMessagePart::Ptr(new EncryptedMessagePart(objectTreeParser, data->decodedText(), QGpgME::openpgp(), node, data));
mp->setIsEncrypted(true);
return mp;
}
}
// we need the intermediate part to preserve the headers (necessary for with protected headers using multipart mixed)
auto part = MessagePart::Ptr(new MessagePart(objectTreeParser, {}, node));
part->appendSubPart(MimeMessagePart::Ptr(new MimeMessagePart(objectTreeParser, contents.at(0), false)));
return part;
}
};
class ApplicationPGPEncryptedBodyPartFormatter : public MimeTreeParser::Interface::BodyPartFormatter
{
public:
MessagePart::Ptr process(ObjectTreeParser *objectTreeParser, KMime::Content *node) const override
{
- if (node->decodedContent().trimmed() != "Version: 1"_ba) {
- qCWarning(MIMETREEPARSER_CORE_LOG) << "Unknown PGP Version String:" << node->decodedContent().trimmed();
+ if (node->decodedBody().trimmed() != "Version: 1"_ba) {
+ qCWarning(MIMETREEPARSER_CORE_LOG) << "Unknown PGP Version String:" << node->decodedBody().trimmed();
}
if (!node->parent()) {
return MessagePart::Ptr();
}
KMime::Content *data = findTypeInDirectChildren(node->parent(), "application/octet-stream"_ba);
if (!data) {
return MessagePart::Ptr(); // new MimeMessagePart(objectTreeParser, node));
}
EncryptedMessagePart::Ptr mp(new EncryptedMessagePart(objectTreeParser, data->decodedText(), QGpgME::openpgp(), node, data));
mp->setIsEncrypted(true);
return mp;
}
};
class ApplicationPkcs7MimeBodyPartFormatter : public MimeTreeParser::Interface::BodyPartFormatter
{
public:
MessagePart::Ptr process(ObjectTreeParser *objectTreeParser, KMime::Content *node) const override
{
if (node->head().isEmpty()) {
return MessagePart::Ptr();
}
const QString smimeType = node->contentType()->parameter("smime-type").toLower();
if (smimeType == QLatin1StringView("certs-only")) {
return CertMessagePart::Ptr(new CertMessagePart(objectTreeParser, node, QGpgME::smime()));
}
bool isSigned = (smimeType == QLatin1StringView("signed-data"));
bool isEncrypted = (smimeType == QLatin1StringView("enveloped-data"));
// Analyze "signTestNode" node to find/verify a signature.
// If zero part.objectTreeParser verification was successfully done after
// decrypting via recursion by insertAndParseNewChildNode().
KMime::Content *signTestNode = isEncrypted ? nullptr : node;
// We try decrypting the content
// if we either *know* that it is an encrypted message part
// or there is neither signed nor encrypted parameter.
MessagePart::Ptr mp;
if (!isSigned) {
if (isEncrypted) {
qCDebug(MIMETREEPARSER_CORE_LOG) << "pkcs7 mime == S/MIME TYPE: enveloped (encrypted) data";
} else {
qCDebug(MIMETREEPARSER_CORE_LOG) << "pkcs7 mime - type unknown - enveloped (encrypted) data ?";
}
auto _mp = EncryptedMessagePart::Ptr(new EncryptedMessagePart(objectTreeParser, node->decodedText(), QGpgME::smime(), node));
mp = _mp;
_mp->setIsEncrypted(true);
// PartMetaData *messagePart(_mp->partMetaData());
// if (!part.source()->decryptMessage()) {
// isEncrypted = true;
signTestNode = nullptr; // PENDING(marc) to be abs. sure, we'd need to have to look at the content
// } else {
// _mp->startDecryption();
// if (messagePart->isDecryptable) {
// qCDebug(MIMETREEPARSER_CORE_LOG) << "pkcs7 mime - encryption found - enveloped (encrypted) data !";
// isEncrypted = true;
// part.nodeHelper()->setEncryptionState(node, KMMsgFullyEncrypted);
// signTestNode = nullptr;
// } else {
// // decryption failed, which could be because the part was encrypted but
// // decryption failed, or because we didn't know if it was encrypted, tried,
// // and failed. If the message was not actually encrypted, we continue
// // assuming it's signed
// if (_mp->passphraseError() || (smimeType.isEmpty() && messagePart->isEncrypted)) {
// isEncrypted = true;
// signTestNode = nullptr;
// }
// if (isEncrypted) {
// qCDebug(MIMETREEPARSER_CORE_LOG) << "pkcs7 mime - ERROR: COULD NOT DECRYPT enveloped data !";
// } else {
// qCDebug(MIMETREEPARSER_CORE_LOG) << "pkcs7 mime - NO encryption found";
// }
// }
// }
}
// We now try signature verification if necessarry.
if (signTestNode) {
if (isSigned) {
qCDebug(MIMETREEPARSER_CORE_LOG) << "pkcs7 mime == S/MIME TYPE: opaque signed data";
} else {
qCDebug(MIMETREEPARSER_CORE_LOG) << "pkcs7 mime - type unknown - opaque signed data ?";
}
return SignedMessagePart::Ptr(new SignedMessagePart(objectTreeParser, QGpgME::smime(), nullptr, signTestNode));
}
return mp;
}
};
class MultiPartAlternativeBodyPartFormatter : public MimeTreeParser::Interface::BodyPartFormatter
{
public:
MessagePart::Ptr process(ObjectTreeParser *objectTreeParser, KMime::Content *node) const override
{
if (node->contents().isEmpty()) {
return MessagePart::Ptr();
}
AlternativeMessagePart::Ptr mp(new AlternativeMessagePart(objectTreeParser, node));
if (mp->mChildParts.isEmpty()) {
return MimeMessagePart::Ptr(new MimeMessagePart(objectTreeParser, node->contents().at(0)));
}
return mp;
}
};
class MultiPartEncryptedBodyPartFormatter : public MimeTreeParser::Interface::BodyPartFormatter
{
public:
MessagePart::Ptr process(ObjectTreeParser *objectTreeParser, KMime::Content *node) const override
{
if (node->contents().isEmpty()) {
Q_ASSERT(false);
return MessagePart::Ptr();
}
const QGpgME::Protocol *protocol = nullptr;
/*
ATTENTION: This code is to be replaced by the new 'auto-detect' feature. --------------------------------------
*/
KMime::Content *data = findTypeInDirectChildren(node, "application/octet-stream"_ba);
if (data) {
protocol = QGpgME::openpgp();
} else {
data = findTypeInDirectChildren(node, "application/pkcs7-mime"_ba);
if (data) {
protocol = QGpgME::smime();
}
}
/*
---------------------------------------------------------------------------------------------------------------
*/
if (!data) {
return MessagePart::Ptr(new MimeMessagePart(objectTreeParser, node->contents().at(0)));
}
EncryptedMessagePart::Ptr mp(new EncryptedMessagePart(objectTreeParser, data->decodedText(), protocol, node, data));
mp->setIsEncrypted(true);
return mp;
}
};
class MultiPartSignedBodyPartFormatter : public MimeTreeParser::Interface::BodyPartFormatter
{
public:
static const QGpgME::Protocol *detectProtocol(const QString &protocolContentType_, const QString &signatureContentType)
{
auto protocolContentType = protocolContentType_;
if (protocolContentType.isEmpty()) {
qCWarning(MIMETREEPARSER_CORE_LOG) << "Message doesn't set the protocol for the multipart/signed content-type, "
"using content-type of the signature:"
<< signatureContentType;
protocolContentType = signatureContentType;
}
const QGpgME::Protocol *protocol = nullptr;
if (protocolContentType == QLatin1StringView("application/pkcs7-signature")
|| protocolContentType == QLatin1StringView("application/x-pkcs7-signature")) {
protocol = QGpgME::smime();
} else if (protocolContentType == QLatin1StringView("application/pgp-signature")
|| protocolContentType == QLatin1StringView("application/x-pgp-signature")) {
protocol = QGpgME::openpgp();
}
return protocol;
}
MessagePart::Ptr process(ObjectTreeParser *objectTreeParser, KMime::Content *node) const override
{
if (node->contents().size() != 2) {
qCDebug(MIMETREEPARSER_CORE_LOG) << "mulitpart/signed must have exactly two child parts!" << Qt::endl << "processing as multipart/mixed";
if (!node->contents().isEmpty()) {
return MessagePart::Ptr(new MimeMessagePart(objectTreeParser, node->contents().at(0)));
} else {
return MessagePart::Ptr();
}
}
KMime::Content *signedData = node->contents().at(0);
KMime::Content *signature = node->contents().at(1);
Q_ASSERT(signedData);
Q_ASSERT(signature);
auto protocol = detectProtocol(node->contentType()->parameter("protocol").toLower(), QLatin1StringView(signature->contentType()->mimeType().toLower()));
if (!protocol) {
return MessagePart::Ptr(new MimeMessagePart(objectTreeParser, signedData));
}
return SignedMessagePart::Ptr(new SignedMessagePart(objectTreeParser, protocol, signature, signedData));
}
};
class TextHtmlBodyPartFormatter : public MimeTreeParser::Interface::BodyPartFormatter
{
public:
MessagePart::Ptr process(ObjectTreeParser *objectTreeParser, KMime::Content *node) const override
{
return HtmlMessagePart::Ptr(new HtmlMessagePart(objectTreeParser, node));
}
};
class TextPlainBodyPartFormatter : public MimeTreeParser::Interface::BodyPartFormatter
{
public:
MessagePart::Ptr process(ObjectTreeParser *objectTreeParser, KMime::Content *node) const override
{
if (KMime::isAttachment(node)) {
return AttachmentMessagePart::Ptr(new AttachmentMessagePart(objectTreeParser, node));
}
return TextMessagePart::Ptr(new TextMessagePart(objectTreeParser, node));
}
};
} // anon namespace
void BodyPartFormatterBaseFactoryPrivate::messageviewer_create_builtin_bodypart_formatters()
{
auto any = new AnyTypeBodyPartFormatter;
auto textPlain = new TextPlainBodyPartFormatter;
auto pkcs7 = new ApplicationPkcs7MimeBodyPartFormatter;
auto pgp = new ApplicationPGPEncryptedBodyPartFormatter;
auto html = new TextHtmlBodyPartFormatter;
auto headers = new HeadersBodyPartFormatter;
auto multipartAlternative = new MultiPartAlternativeBodyPartFormatter;
auto multipartMixed = new MultiPartMixedBodyPartFormatter;
auto multipartSigned = new MultiPartSignedBodyPartFormatter;
auto multipartEncrypted = new MultiPartEncryptedBodyPartFormatter;
auto message = new MessageRfc822BodyPartFormatter;
auto multipartRelated = new MultiPartRelatedBodyPartFormatter;
insert("application", "octet-stream", any);
insert("application", "pgp", textPlain);
insert("application", "pkcs7-mime", pkcs7);
insert("application", "x-pkcs7-mime", pkcs7);
insert("application", "pgp-encrypted", pgp);
insert("application", "*", any);
insert("text", "html", html);
insert("text", "rtf", any);
insert("text", "plain", textPlain);
insert("text", "rfc822-headers", headers);
insert("text", "*", textPlain);
insert("image", "*", any);
insert("message", "rfc822", message);
insert("message", "*", any);
insert("multipart", "alternative", multipartAlternative);
insert("multipart", "encrypted", multipartEncrypted);
insert("multipart", "signed", multipartSigned);
insert("multipart", "related", multipartRelated);
insert("multipart", "*", multipartMixed);
insert("*", "*", any);
}
diff --git a/src/core/cryptohelper.cpp b/src/core/cryptohelper.cpp
index 5459f8e..97b3351 100644
--- a/src/core/cryptohelper.cpp
+++ b/src/core/cryptohelper.cpp
@@ -1,312 +1,312 @@
// SPDX-FileCopyrightText: 2001,2002 the KPGP authors
// SPDX-FileCopyrightText: 2015 Sandro Knauß <knauss@kolabsys.com>
// SPDX-FileCopyrightText: 2017 Daniel Vrátil <dvratil@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "cryptohelper.h"
#include "mimetreeparser_core_debug.h"
#include <QGpgME/DecryptJob>
#include <QGpgME/DecryptVerifyJob>
#include <QGpgME/Protocol>
#include <Libkleo/Formatting>
#include <gpgme++/context.h>
#include <gpgme++/decryptionresult.h>
#include <gpgme++/verificationresult.h>
using namespace MimeTreeParser;
PGPBlockType Block::determineType() const
{
const QByteArray data = text();
if (data.startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----")) {
return NoPgpBlock;
} else if (data.startsWith("-----BEGIN PGP SIGNED")) {
return ClearsignedBlock;
} else if (data.startsWith("-----BEGIN PGP SIGNATURE")) {
return SignatureBlock;
} else if (data.startsWith("-----BEGIN PGP PUBLIC")) {
return PublicKeyBlock;
} else if (data.startsWith("-----BEGIN PGP PRIVATE") || data.startsWith("-----BEGIN PGP SECRET")) {
return PrivateKeyBlock;
} else if (data.startsWith("-----BEGIN PGP MESSAGE")) {
if (data.startsWith("-----BEGIN PGP MESSAGE PART")) {
return MultiPgpMessageBlock;
} else {
return PgpMessageBlock;
}
} else if (data.startsWith("-----BEGIN PGP ARMORED FILE")) {
return PgpMessageBlock;
} else if (data.startsWith("-----BEGIN PGP ")) {
return UnknownBlock;
} else {
return NoPgpBlock;
}
}
QList<Block> MimeTreeParser::prepareMessageForDecryption(const QByteArray &msg)
{
PGPBlockType pgpBlock = NoPgpBlock;
QList<Block> blocks;
int start = -1; // start of the current PGP block
int lastEnd = -1; // end of the last PGP block
const int length = msg.length();
if (msg.isEmpty()) {
return blocks;
}
if (msg.startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----")) {
return blocks;
}
if (msg.startsWith("-----BEGIN PGP ")) {
start = 0;
} else {
start = msg.indexOf("\n-----BEGIN PGP ") + 1;
if (start == 0) {
blocks.append(Block(msg, NoPgpBlock));
return blocks;
}
}
while (start != -1) {
int nextEnd;
int nextStart;
// is the PGP block a clearsigned block?
if (!strncmp(msg.constData() + start + 15, "SIGNED", 6)) {
pgpBlock = ClearsignedBlock;
} else {
pgpBlock = UnknownBlock;
}
nextEnd = msg.indexOf("\n-----END PGP ", start + 15);
nextStart = msg.indexOf("\n-----BEGIN PGP ", start + 15);
if (nextEnd == -1) { // Missing END PGP line
if (lastEnd != -1) {
blocks.append(Block(msg.mid(lastEnd + 1), UnknownBlock));
} else {
blocks.append(Block(msg.mid(start), UnknownBlock));
}
break;
}
if ((nextStart == -1) || (nextEnd < nextStart) || (pgpBlock == ClearsignedBlock)) {
// most likely we found a PGP block (but we don't check if it's valid)
// store the preceding non-PGP block
if (start - lastEnd - 1 > 0) {
blocks.append(Block(msg.mid(lastEnd + 1, start - lastEnd - 1), NoPgpBlock));
}
lastEnd = msg.indexOf("\n", nextEnd + 14);
if (lastEnd == -1) {
if (start < length) {
blocks.append(Block(msg.mid(start)));
}
break;
} else {
blocks.append(Block(msg.mid(start, lastEnd + 1 - start)));
if ((nextStart != -1) && (nextEnd > nextStart)) {
nextStart = msg.indexOf("\n-----BEGIN PGP ", lastEnd + 1);
}
}
}
start = nextStart;
if (start == -1) {
if (lastEnd + 1 < length) {
// rest of mail is no PGP Block
blocks.append(Block(msg.mid(lastEnd + 1), NoPgpBlock));
}
break;
} else {
start++; // move start behind the '\n'
}
}
return blocks;
}
Block::Block() = default;
Block::Block(const QByteArray &m)
: msg(m)
{
mType = determineType();
}
Block::Block(const QByteArray &m, PGPBlockType t)
: msg(m)
, mType(t)
{
}
QByteArray MimeTreeParser::Block::text() const
{
return msg;
}
PGPBlockType Block::type() const
{
return mType;
}
namespace
{
[[nodiscard]] bool isPGP(const KMime::Content *part, bool allowOctetStream = false)
{
const auto ct = static_cast<KMime::Headers::ContentType *>(part->headerByType("Content-Type"));
return ct && (ct->isSubtype("pgp-encrypted") || ct->isSubtype("encrypted") || (allowOctetStream && ct->isMimeType("application/octet-stream")));
}
[[nodiscard]] bool isSMIME(const KMime::Content *part)
{
const auto ct = static_cast<KMime::Headers::ContentType *>(part->headerByType("Content-Type"));
return ct && (ct->isSubtype("pkcs7-mime") || ct->isSubtype("x-pkcs7-mime"));
}
void copyHeader(const KMime::Headers::Base *header, KMime::Message::Ptr msg)
{
auto newHdr = KMime::Headers::createHeader(header->type());
if (!newHdr) {
newHdr = new KMime::Headers::Generic(header->type());
}
newHdr->from7BitString(header->as7BitString(false));
msg->appendHeader(newHdr);
}
[[nodiscard]] bool isContentHeader(const KMime::Headers::Base *header)
{
return header->is("Content-Type") || header->is("Content-Transfer-Encoding") || header->is("Content-Disposition");
}
[[nodiscard]] KMime::Message::Ptr assembleMessage(const KMime::Message::Ptr &orig, const KMime::Content *newContent)
{
auto out = KMime::Message::Ptr::create();
// Use the new content as message content
out->setBody(const_cast<KMime::Content *>(newContent)->encodedBody());
out->parse();
// remove default explicit content headers added by KMime::Content::parse()
QList<KMime::Headers::Base *> headers = out->headers();
for (const auto hdr : std::as_const(headers)) {
if (isContentHeader(hdr)) {
out->removeHeader(hdr->type());
}
}
// Copy over headers from the original message, except for CT, CTE and CD
// headers, we want to preserve those from the new content
headers = orig->headers();
for (const auto hdr : std::as_const(headers)) {
if (isContentHeader(hdr)) {
continue;
}
copyHeader(hdr, out);
}
// Overwrite some headers by those provided by the new content;
const auto newContentHeaders = newContent->headers();
for (const auto hdr : newContentHeaders) {
if (isContentHeader(hdr)) {
copyHeader(hdr, out);
}
}
out->assemble();
out->parse();
return out;
}
}
KMime::Message::Ptr CryptoUtils::decryptMessage(const KMime::Message::Ptr &msg, bool &wasEncrypted, GpgME::Protocol &protoName)
{
protoName = GpgME::UnknownProtocol;
bool multipart = false;
if (msg->contentType(false) && msg->contentType(false)->isMimeType("multipart/encrypted")) {
multipart = true;
const auto subparts = msg->contents();
for (auto subpart : subparts) {
if (isPGP(subpart, true)) {
protoName = GpgME::OpenPGP;
break;
} else if (isSMIME(subpart)) {
protoName = GpgME::CMS;
break;
}
}
} else {
if (isPGP(msg.data())) {
protoName = GpgME::OpenPGP;
} else if (isSMIME(msg.data())) {
protoName = GpgME::CMS;
} else {
- const auto blocks = prepareMessageForDecryption(msg->decodedContent());
+ const auto blocks = prepareMessageForDecryption(msg->decodedBody());
QByteArray content;
for (const auto &block : blocks) {
if (block.type() == PgpMessageBlock) {
const auto proto = QGpgME::openpgp();
protoName = GpgME::OpenPGP;
wasEncrypted = true;
QByteArray outData;
const auto decryptVerify = proto->decryptVerifyJob();
const auto [decryptResult, verifyResult] = decryptVerify->exec(block.text(), outData);
if (decryptResult.error()) {
// unknown key, invalid algo, or general error
qCWarning(MIMETREEPARSER_CORE_LOG) << "Failed to decrypt:" << Kleo::Formatting::errorAsString(decryptResult.error());
return {};
} else if (verifyResult.error()) {
qCWarning(MIMETREEPARSER_CORE_LOG) << "Failed to verify:" << Kleo::Formatting::errorAsString(verifyResult.error());
return {};
}
content += KMime::CRLFtoLF(outData);
} else if (block.type() == NoPgpBlock) {
content += block.text();
}
}
KMime::Content decCt;
decCt.setBody(content);
decCt.parse();
decCt.assemble();
return assembleMessage(msg, &decCt);
}
}
if (protoName == GpgME::UnknownProtocol) {
wasEncrypted = false;
qCWarning(MIMETREEPARSER_CORE_LOG) << "Not encrypted, or we don't recognize the encryption";
return {};
}
const auto proto = (protoName == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime();
wasEncrypted = true;
QByteArray outData;
- auto inData = multipart ? msg->encodedContent() : msg->decodedContent(); // decodedContent in fact returns decoded body
+ auto inData = multipart ? msg->encodedContent() : msg->decodedBody(); // decodedContent in fact returns decoded body
auto decrypt = proto->decryptJob();
auto result = decrypt->exec(inData, outData);
if (result.error()) {
// unknown key, invalid algo, or general error
qCWarning(MIMETREEPARSER_CORE_LOG) << "Failed to decrypt:" << Kleo::Formatting::errorAsString(result.error());
return {};
}
KMime::Content decCt;
decCt.setContent(KMime::CRLFtoLF(outData));
decCt.parse();
decCt.assemble();
return assembleMessage(msg, &decCt);
}
diff --git a/src/core/messageparser.cpp b/src/core/messageparser.cpp
index c77071b..a64acb6 100644
--- a/src/core/messageparser.cpp
+++ b/src/core/messageparser.cpp
@@ -1,243 +1,243 @@
// SPDX-FileCopyrightText: 2016 Christian Mollekopf <mollekopf@kolabsys.com>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "messageparser.h"
using namespace Qt::Literals::StringLiterals;
#include "attachmentmodel.h"
#include "mimetreeparser_core_debug.h"
#include "objecttreeparser.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 << u"%1 <%2>"_s.arg(mailbox.name().toHtmlEscaped(), QString::fromUtf8(mailbox.address()).toHtmlEscaped());
} else if (mailbox.hasAddress()) {
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->decryptAndVerify();
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("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->setContent(part->node()->decodedBody());
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->mailbox()});
}
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();
}
#include "moc_messageparser.cpp"
diff --git a/src/core/messagepart.cpp b/src/core/messagepart.cpp
index 4e01358..efaabc9 100644
--- a/src/core/messagepart.cpp
+++ b/src/core/messagepart.cpp
@@ -1,969 +1,969 @@
// SPDX-FileCopyrightText: 2015 Sandro Knauß <knauss@kolabsys.com>
// SPDX-FileCopyrightText: 2017 Christian Mollekopf <mollekopf@kolabsys.com>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "messagepart.h"
#include "cryptohelper.h"
#include "mimetreeparser_core_debug.h"
#include "objecttreeparser.h"
#include "utils.h"
#include <KLocalizedString>
#include <KMime/Content>
#include <Libkleo/Compliance>
#include <Libkleo/Formatting>
#include <Libkleo/KeyCache>
#include <QGpgME/DN>
#include <QGpgME/DecryptVerifyJob>
#include <QGpgME/Protocol>
#include <QGpgME/VerifyDetachedJob>
#include <QGpgME/VerifyOpaqueJob>
#include <QStringDecoder>
#include <gpgme++/key.h>
#include <gpgme++/keylistresult.h>
#include <gpgme.h>
using namespace Qt::Literals::StringLiterals;
using namespace MimeTreeParser;
//------MessagePart-----------------------
MessagePart::MessagePart(ObjectTreeParser *otp, const QString &text, KMime::Content *node)
: mText(text)
, mOtp(otp)
, mParentPart(nullptr)
, mNode(node) // only null for messagepartlist
, mError(NoError)
, mRoot(false)
{
}
MessagePart::~MessagePart()
{
for (auto n : std::as_const(mNodesToDelete)) {
delete n;
}
}
MessagePart::Disposition MessagePart::disposition() const
{
if (!mNode) {
return Invalid;
}
const auto cd = mNode->contentDisposition(false);
if (!cd) {
return Invalid;
}
switch (cd->disposition()) {
case KMime::Headers::CDinline:
return Inline;
case KMime::Headers::CDattachment:
return Attachment;
default:
return Invalid;
}
}
QString MessagePart::filename() const
{
if (!mNode) {
return {};
}
if (const auto cd = mNode->contentDisposition(false)) {
const auto name = cd->filename();
// Allow for a fallback for mails that have a ContentDisposition header, but don't set the filename anyways.
// Not the recommended way, but exists.
if (!name.isEmpty()) {
return name;
}
}
if (const auto ct = mNode->contentType(false)) {
return ct->name();
}
return {};
}
static KMime::Headers::ContentType *contentType(KMime::Content *node)
{
if (node) {
return node->contentType(false);
}
return nullptr;
}
QByteArray MessagePart::charset() const
{
if (!mNode) {
return "us-ascii"_ba;
}
if (auto ct = contentType(mNode)) {
return ct->charset();
}
// Per rfc2045 us-ascii is the default
return "us-ascii"_ba;
}
QByteArray MessagePart::mimeType() const
{
if (auto ct = contentType(mNode)) {
return ct->mimeType();
}
return {};
}
bool MessagePart::isText() const
{
if (auto ct = contentType(mNode)) {
return ct->isText();
}
return false;
}
MessagePart::Error MessagePart::error() const
{
return mError;
}
QString MessagePart::errorString() const
{
return mMetaData.errorText;
}
PartMetaData *MessagePart::partMetaData()
{
return &mMetaData;
}
bool MessagePart::isAttachment() const
{
if (mNode) {
return KMime::isAttachment(mNode);
}
return false;
}
KMime::Content *MessagePart::node() const
{
return mNode;
}
void MessagePart::setIsRoot(bool root)
{
mRoot = root;
}
bool MessagePart::isRoot() const
{
return mRoot;
}
QString MessagePart::text() const
{
return mText;
}
void MessagePart::setText(const QString &text)
{
mText = text;
}
bool MessagePart::isHtml() const
{
return false;
}
MessagePart *MessagePart::parentPart() const
{
return mParentPart;
}
void MessagePart::setParentPart(MessagePart *parentPart)
{
mParentPart = parentPart;
}
QString MessagePart::htmlContent() const
{
return text();
}
QString MessagePart::plaintextContent() const
{
return text();
}
void MessagePart::parseInternal(KMime::Content *node, bool onlyOneMimePart)
{
auto subMessagePart = mOtp->parseObjectTreeInternal(node, onlyOneMimePart);
mRoot = subMessagePart->isRoot();
for (const auto &part : std::as_const(subMessagePart->subParts())) {
appendSubPart(part);
}
}
void MessagePart::parseInternal(const QByteArray &data)
{
auto tempNode = new KMime::Content();
const auto lfData = KMime::CRLFtoLF(data);
// We have to deal with both bodies and full parts. In inline encrypted/signed parts we can have nested parts,
// or just plain-text, and both ends up here. setContent defaults to setting only the header, so we have to avoid this.
if (lfData.contains("\n\n")) {
tempNode->setContent(lfData);
} else {
tempNode->setBody(lfData);
}
tempNode->parse();
tempNode->contentType()->setCharset(charset());
bindLifetime(tempNode);
if (!tempNode->head().isEmpty()) {
tempNode->contentDescription()->from7BitString("temporary node"_ba);
}
parseInternal(tempNode);
}
QString MessagePart::renderInternalText() const
{
QString text;
for (const auto &mp : subParts()) {
text += mp->text();
}
return text;
}
void MessagePart::appendSubPart(const MessagePart::Ptr &messagePart)
{
messagePart->setParentPart(this);
mBlocks.append(messagePart);
}
const QList<MessagePart::Ptr> &MessagePart::subParts() const
{
return mBlocks;
}
bool MessagePart::hasSubParts() const
{
return !mBlocks.isEmpty();
}
QList<SignedMessagePart *> MessagePart::signatures() const
{
QList<SignedMessagePart *> list;
if (auto sig = dynamic_cast<SignedMessagePart *>(const_cast<MessagePart *>(this))) {
list << sig;
}
auto parent = parentPart();
while (parent) {
if (auto sig = dynamic_cast<SignedMessagePart *>(parent)) {
list << sig;
}
parent = parent->parentPart();
}
return list;
}
QList<EncryptedMessagePart *> MessagePart::encryptions() const
{
QList<EncryptedMessagePart *> list;
if (auto sig = dynamic_cast<EncryptedMessagePart *>(const_cast<MessagePart *>(this))) {
list << sig;
}
auto parent = parentPart();
while (parent) {
if (auto sig = dynamic_cast<EncryptedMessagePart *>(parent)) {
list << sig;
}
parent = parent->parentPart();
}
return list;
}
KMMsgEncryptionState MessagePart::encryptionState() const
{
if (!encryptions().isEmpty()) {
return KMMsgFullyEncrypted;
}
return KMMsgNotEncrypted;
}
KMMsgSignatureState MessagePart::signatureState() const
{
if (!signatures().isEmpty()) {
return KMMsgFullySigned;
}
return KMMsgNotSigned;
}
void MessagePart::bindLifetime(KMime::Content *node)
{
mNodesToDelete << node;
}
KMime::Headers::Base *MimeTreeParser::MessagePart::header(const char *header) const
{
if (node() && node()->hasHeader(header)) {
return node()->headerByType(header);
}
if (auto parent = parentPart()) {
return parent->header(header);
}
return nullptr;
}
//-----MessagePartList----------------------
MessagePartList::MessagePartList(ObjectTreeParser *otp, KMime::Content *node)
: MessagePart(otp, QString(), node)
{
}
QString MessagePartList::text() const
{
return renderInternalText();
}
QString MessagePartList::plaintextContent() const
{
return QString();
}
QString MessagePartList::htmlContent() const
{
return QString();
}
//-----TextMessageBlock----------------------
TextMessagePart::TextMessagePart(ObjectTreeParser *otp, KMime::Content *node)
: MessagePartList(otp, node)
, mSignatureState(KMMsgSignatureStateUnknown)
, mEncryptionState(KMMsgEncryptionStateUnknown)
{
if (!mNode) {
qCWarning(MIMETREEPARSER_CORE_LOG) << "not a valid node";
return;
}
parseContent();
}
void TextMessagePart::parseContent()
{
mSignatureState = KMMsgNotSigned;
mEncryptionState = KMMsgNotEncrypted;
- const auto blocks = prepareMessageForDecryption(mNode->decodedContent());
+ const auto blocks = prepareMessageForDecryption(mNode->decodedBody());
// We also get blocks for unencrypted messages
if (!blocks.isEmpty()) {
auto aCodec = QStringDecoder(mOtp->codecNameFor(mNode).constData());
const auto cryptProto = QGpgME::openpgp();
/* The (overall) signature/encrypted status is broken
* if one unencrypted part is at the beginning or in the middle
* because mailmain adds an unencrypted part at the end this should not break the overall status
*
* That's why we first set the tmp status and if one crypted/signed block comes afterwards, than
* the status is set to unencryped
*/
bool fullySignedOrEncrypted = true;
bool fullySignedOrEncryptedTmp = true;
for (const auto &block : blocks) {
if (!fullySignedOrEncryptedTmp) {
fullySignedOrEncrypted = false;
}
if (block.type() == NoPgpBlock && !block.text().trimmed().isEmpty()) {
fullySignedOrEncryptedTmp = false;
appendSubPart(MessagePart::Ptr(new MessagePart(mOtp, aCodec.decode(KMime::CRLFtoLF(block.text())))));
} else if (block.type() == PgpMessageBlock) {
auto content = new KMime::Content;
content->setBody(block.text());
content->parse();
content->contentType()->setCharset(charset());
EncryptedMessagePart::Ptr mp(new EncryptedMessagePart(mOtp, QString(), cryptProto, content, content, false));
mp->bindLifetime(content);
mp->setIsEncrypted(true);
appendSubPart(mp);
} else if (block.type() == ClearsignedBlock) {
auto content = new KMime::Content;
content->setBody(block.text());
content->parse();
content->contentType()->setCharset(charset());
SignedMessagePart::Ptr mp(new SignedMessagePart(mOtp, cryptProto, nullptr, content, false));
mp->bindLifetime(content);
appendSubPart(mp);
} else {
continue;
}
const auto mp = subParts().last().staticCast<MessagePart>();
const PartMetaData *messagePart(mp->partMetaData());
if (!messagePart->isEncrypted && !messagePart->isSigned() && !block.text().trimmed().isEmpty()) {
mp->setText(aCodec.decode(KMime::CRLFtoLF(block.text())));
}
if (messagePart->isEncrypted) {
mEncryptionState = KMMsgPartiallyEncrypted;
}
if (messagePart->isSigned()) {
mSignatureState = KMMsgPartiallySigned;
}
}
// Do we have an fully Signed/Encrypted Message?
if (fullySignedOrEncrypted) {
if (mSignatureState == KMMsgPartiallySigned) {
mSignatureState = KMMsgFullySigned;
}
if (mEncryptionState == KMMsgPartiallyEncrypted) {
mEncryptionState = KMMsgFullyEncrypted;
}
}
}
}
KMMsgEncryptionState TextMessagePart::encryptionState() const
{
if (mEncryptionState == KMMsgNotEncrypted) {
return MessagePart::encryptionState();
}
return mEncryptionState;
}
KMMsgSignatureState TextMessagePart::signatureState() const
{
if (mSignatureState == KMMsgNotSigned) {
return MessagePart::signatureState();
}
return mSignatureState;
}
//-----AttachmentMessageBlock----------------------
AttachmentMessagePart::AttachmentMessagePart(ObjectTreeParser *otp, KMime::Content *node)
: TextMessagePart(otp, node)
{
}
//-----HtmlMessageBlock----------------------
HtmlMessagePart::HtmlMessagePart(ObjectTreeParser *otp, KMime::Content *node)
: MessagePart(otp, QString(), node)
{
if (!mNode) {
qCWarning(MIMETREEPARSER_CORE_LOG) << "not a valid node";
return;
}
- setText(QStringDecoder(mOtp->codecNameFor(mNode).constData()).decode(KMime::CRLFtoLF(mNode->decodedContent())));
+ setText(QStringDecoder(mOtp->codecNameFor(mNode).constData()).decode(KMime::CRLFtoLF(mNode->decodedBody())));
}
//-----MimeMessageBlock----------------------
MimeMessagePart::MimeMessagePart(ObjectTreeParser *otp, KMime::Content *node, bool onlyOneMimePart)
: MessagePart(otp, QString(), node)
{
if (!mNode) {
qCWarning(MIMETREEPARSER_CORE_LOG) << "not a valid node";
return;
}
parseInternal(mNode, onlyOneMimePart);
}
MimeMessagePart::~MimeMessagePart()
{
}
QString MimeMessagePart::text() const
{
return renderInternalText();
}
QString MimeMessagePart::plaintextContent() const
{
return QString();
}
QString MimeMessagePart::htmlContent() const
{
return QString();
}
//-----AlternativeMessagePart----------------------
AlternativeMessagePart::AlternativeMessagePart(ObjectTreeParser *otp, KMime::Content *node)
: MessagePart(otp, QString(), node)
{
if (auto dataIcal = findTypeInDirectChildren(mNode, "text/calendar")) {
mChildParts[MultipartIcal] = MimeMessagePart::Ptr(new MimeMessagePart(mOtp, dataIcal, true));
}
if (auto dataText = findTypeInDirectChildren(mNode, "text/plain")) {
mChildParts[MultipartPlain] = MimeMessagePart::Ptr(new MimeMessagePart(mOtp, dataText, true));
}
if (auto dataHtml = findTypeInDirectChildren(mNode, "text/html")) {
mChildParts[MultipartHtml] = MimeMessagePart::Ptr(new MimeMessagePart(mOtp, dataHtml, true));
} else {
// If we didn't find the HTML part as the first child of the multipart/alternative, it might
// be that this is a HTML message with images, and text/plain and multipart/related are the
// immediate children of this multipart/alternative node.
// In this case, the HTML node is a child of multipart/related.
// In the case of multipart/related we don't expect multiple html parts, it is usually used to group attachments
// with html content.
//
// In any case, this is not a complete implementation of MIME, but an approximation for the kind of mails we actually see in the wild.
auto data = [&] {
if (auto d = findTypeInDirectChildren(mNode, "multipart/related")) {
return d;
}
return findTypeInDirectChildren(mNode, "multipart/mixed");
}();
if (data) {
QString htmlContent;
const auto parts = data->contents();
for (auto p : parts) {
if ((!p->contentType()->isEmpty()) && (p->contentType()->mimeType() == "text/html")) {
htmlContent += MimeMessagePart(mOtp, p, true).text();
} else if (KMime::isAttachment(p)) {
appendSubPart(MimeMessagePart::Ptr(new MimeMessagePart(otp, p, true)));
}
}
mChildParts[MultipartHtml] = MessagePart::Ptr(new MessagePart(mOtp, htmlContent, nullptr));
}
}
}
AlternativeMessagePart::~AlternativeMessagePart()
{
}
QList<AlternativeMessagePart::HtmlMode> AlternativeMessagePart::availableModes()
{
return mChildParts.keys();
}
QString AlternativeMessagePart::text() const
{
if (mChildParts.contains(MultipartPlain)) {
return mChildParts[MultipartPlain]->text();
}
return QString();
}
bool AlternativeMessagePart::isHtml() const
{
return mChildParts.contains(MultipartHtml);
}
QString AlternativeMessagePart::plaintextContent() const
{
return text();
}
QString AlternativeMessagePart::htmlContent() const
{
if (mChildParts.contains(MultipartHtml)) {
return mChildParts[MultipartHtml]->text();
} else {
return plaintextContent();
}
}
QString AlternativeMessagePart::icalContent() const
{
if (mChildParts.contains(MultipartIcal)) {
return mChildParts[MultipartIcal]->text();
}
return {};
}
//-----CertMessageBlock----------------------
CertMessagePart::CertMessagePart(ObjectTreeParser *otp, KMime::Content *node, QGpgME::Protocol *cryptoProto)
: MessagePart(otp, QString(), node)
, mCryptoProto(cryptoProto)
{
if (!mNode) {
qCWarning(MIMETREEPARSER_CORE_LOG) << "not a valid node";
return;
}
}
CertMessagePart::~CertMessagePart()
{
}
QString CertMessagePart::text() const
{
return QString();
}
//-----SignedMessageBlock---------------------
SignedMessagePart::SignedMessagePart(ObjectTreeParser *otp,
const QGpgME::Protocol *cryptoProto,
KMime::Content *node,
KMime::Content *signedData,
bool parseAfterDecryption)
: MessagePart(otp, {}, node)
, mParseAfterDecryption(parseAfterDecryption)
, mCryptoProto(cryptoProto)
, mSignedData(signedData)
{
mMetaData.status = i18ndc("mimetreeparser", "@info:status", "Wrong Crypto Plug-In.");
}
SignedMessagePart::~SignedMessagePart() = default;
const QGpgME::Protocol *SignedMessagePart::cryptoProto() const
{
return mCryptoProto;
}
void SignedMessagePart::startVerification()
{
if (!mSignedData) {
return;
}
mMetaData.status = i18ndc("mimetreeparser", "@info:status", "Wrong Crypto Plug-In.");
mMetaData.isEncrypted = false;
mMetaData.isDecryptable = false;
auto codec = QStringDecoder(mOtp->codecNameFor(mSignedData).constData());
// If we have a mNode, this is a detached signature
if (mNode) {
- const auto signature = mNode->decodedContent();
+ const auto signature = mNode->decodedBody();
// This is necessary in case the original data contained CRLF's. Otherwise the signature will not match the data (since KMIME normalizes to LF)
const QByteArray signedData = KMime::LFtoCRLF(mSignedData->encodedContent());
const auto job = mCryptoProto->verifyDetachedJob();
setVerificationResult(job->exec(signature, signedData), signedData);
job->deleteLater();
setText(codec.decode(KMime::CRLFtoLF(signedData)));
} else {
QByteArray outdata;
const auto job = mCryptoProto->verifyOpaqueJob();
- setVerificationResult(job->exec(mSignedData->decodedContent(), outdata), outdata);
+ setVerificationResult(job->exec(mSignedData->decodedBody(), outdata), outdata);
job->deleteLater();
setText(codec.decode(KMime::CRLFtoLF(outdata)));
}
}
void SignedMessagePart::setVerificationResult(const GpgME::VerificationResult &result, const QByteArray &signedData)
{
mMetaData.verificationResult = result;
if (mMetaData.isSigned()) {
if (!signedData.isEmpty() && mParseAfterDecryption) {
parseInternal(signedData);
}
}
}
QString SignedMessagePart::plaintextContent() const
{
if (!mNode) {
return MessagePart::text();
} else {
return QString();
}
}
QString SignedMessagePart::htmlContent() const
{
if (!mNode) {
return MessagePart::text();
} else {
return QString();
}
}
//-----CryptMessageBlock---------------------
EncryptedMessagePart::EncryptedMessagePart(ObjectTreeParser *otp,
const QString &text,
const QGpgME::Protocol *cryptoProto,
KMime::Content *node,
KMime::Content *encryptedNode,
bool parseAfterDecryption)
: MessagePart(otp, text, node)
, mParseAfterDecryption(parseAfterDecryption)
, mPassphraseError(false)
, mNoSecKey(false)
, mDecryptMessage(false)
, mCryptoProto(cryptoProto)
, mEncryptedNode(encryptedNode)
{
mMetaData.isEncrypted = false;
mMetaData.isDecryptable = false;
mMetaData.status = i18ndc("mimetreeparser", "@info:status", "Wrong Crypto Plug-In.");
}
void EncryptedMessagePart::setIsEncrypted(bool encrypted)
{
mMetaData.isEncrypted = encrypted;
}
bool EncryptedMessagePart::isEncrypted() const
{
return mMetaData.isEncrypted;
}
const QGpgME::Protocol *EncryptedMessagePart::cryptoProto() const
{
return mCryptoProto;
}
void EncryptedMessagePart::setDecryptMessage(bool decrypt)
{
mDecryptMessage = decrypt;
}
bool EncryptedMessagePart::decryptMessage() const
{
return mDecryptMessage;
}
bool EncryptedMessagePart::isDecryptable() const
{
return mMetaData.isDecryptable;
}
bool EncryptedMessagePart::isNoSecKey() const
{
return mNoSecKey;
}
bool EncryptedMessagePart::passphraseError() const
{
return mPassphraseError;
}
bool EncryptedMessagePart::decrypt(KMime::Content &data)
{
mError = NoError;
mMetaData.errorText.clear();
// FIXME
// mMetaData.auditLogError = GpgME::Error();
mMetaData.auditLog.clear();
- const QByteArray ciphertext = data.decodedContent();
+ const QByteArray ciphertext = data.decodedBody();
QByteArray plainText;
auto job = mCryptoProto->decryptVerifyJob();
const std::pair<GpgME::DecryptionResult, GpgME::VerificationResult> p = job->exec(ciphertext, plainText);
job->deleteLater();
auto decryptResult = p.first;
auto verifyResult = p.second;
mMetaData.verificationResult = verifyResult;
// Normalize CRLF's
plainText = KMime::CRLFtoLF(plainText);
auto codec = QStringDecoder(mOtp->codecNameFor(&data).constData());
const auto decoded = codec.decode(plainText);
if (partMetaData()->isSigned()) {
// We simply attach a signed message part to indicate that this content is also signed
// We're forwarding mNode to not loose the encoding information
auto subPart = SignedMessagePart::Ptr(new SignedMessagePart(mOtp, mCryptoProto, mNode, nullptr));
subPart->setText(decoded);
subPart->setVerificationResult(verifyResult, plainText);
appendSubPart(subPart);
}
mDecryptRecipients.clear();
bool cannotDecrypt = false;
bool bDecryptionOk = !decryptResult.error();
for (const auto &recipient : decryptResult.recipients()) {
if (!recipient.status()) {
bDecryptionOk = true;
}
GpgME::Key key;
key = Kleo::KeyCache::instance()->findByKeyIDOrFingerprint(recipient.keyID());
if (key.isNull()) {
auto ret = Kleo::KeyCache::instance()->findSubkeysByKeyID({recipient.keyID()});
if (ret.size() == 1) {
key = ret.front().parent();
}
if (key.isNull()) {
qCDebug(MIMETREEPARSER_CORE_LOG) << "Found no Key for KeyID " << recipient.keyID();
}
}
mDecryptRecipients.emplace_back(recipient, key);
}
if (!bDecryptionOk && partMetaData()->isSigned()) {
// Only a signed part
partMetaData()->isEncrypted = false;
bDecryptionOk = true;
mDecryptedData = plainText;
} else {
mPassphraseError = decryptResult.error().isCanceled() || decryptResult.error().code() == GPG_ERR_BAD_PASSPHRASE;
mMetaData.isEncrypted = bDecryptionOk || decryptResult.error().code() != GPG_ERR_NO_DATA;
if (decryptResult.error().isCanceled()) {
setDecryptMessage(false);
}
partMetaData()->errorText = Kleo::Formatting::errorAsString(decryptResult.error());
if (Kleo::DeVSCompliance::isCompliant()) {
partMetaData()->isCompliant = decryptResult.isDeVs();
partMetaData()->compliance = Kleo::DeVSCompliance::name(decryptResult.isDeVs());
} else {
partMetaData()->isCompliant = true;
}
if (partMetaData()->isEncrypted && decryptResult.numRecipients() > 0) {
partMetaData()->keyId = decryptResult.recipient(0).keyID();
}
if (bDecryptionOk) {
mDecryptedData = plainText;
} else {
mNoSecKey = true;
const auto decryRecipients = decryptResult.recipients();
for (const GpgME::DecryptionResult::Recipient &recipient : decryRecipients) {
mNoSecKey &= (recipient.status().code() == GPG_ERR_NO_SECKEY);
}
if (!mPassphraseError && !mNoSecKey) { // GpgME do not detect passphrase error correctly
mPassphraseError = true;
}
}
}
if (!bDecryptionOk) {
QString cryptPlugLibName;
mError = UnknownError;
if (mCryptoProto) {
cryptPlugLibName = mCryptoProto->name();
}
if (mNoSecKey) {
mError = NoKeyError;
}
if (mPassphraseError) {
mError = PassphraseError;
}
if (!mCryptoProto) {
partMetaData()->errorText = i18n("No appropriate crypto plug-in was found.");
} else if (cannotDecrypt) {
partMetaData()->errorText = i18n("Crypto plug-in \"%1\" cannot decrypt messages.", cryptPlugLibName);
} else if (!passphraseError()) {
partMetaData()->errorText = i18n("Crypto plug-in \"%1\" could not decrypt the data.", cryptPlugLibName) + QLatin1StringView("<br />")
+ i18n("Error: %1", partMetaData()->errorText);
}
}
return bDecryptionOk;
}
void EncryptedMessagePart::startDecryption(KMime::Content *data)
{
mMetaData.isEncrypted = true;
mMetaData.isDecryptable = decrypt(*data);
if (mParseAfterDecryption && !mMetaData.isSigned()) {
parseInternal(mDecryptedData);
} else {
setText(QString::fromUtf8(mDecryptedData.constData()));
}
}
void EncryptedMessagePart::startDecryption()
{
if (mEncryptedNode) {
startDecryption(mEncryptedNode);
} else {
startDecryption(mNode);
}
}
std::vector<std::pair<GpgME::DecryptionResult::Recipient, GpgME::Key>> EncryptedMessagePart::decryptRecipients() const
{
return mDecryptRecipients;
}
QString EncryptedMessagePart::plaintextContent() const
{
if (!mNode) {
return MessagePart::text();
} else {
return QString();
}
}
QString EncryptedMessagePart::htmlContent() const
{
if (!mNode) {
return MessagePart::text();
} else {
return QString();
}
}
QString EncryptedMessagePart::text() const
{
if (hasSubParts()) {
auto _mp = (subParts()[0]).dynamicCast<SignedMessagePart>();
if (_mp) {
return _mp->text();
}
}
return MessagePart::text();
}
EncapsulatedRfc822MessagePart::EncapsulatedRfc822MessagePart(ObjectTreeParser *otp, KMime::Content *node, const KMime::Message::Ptr &message)
: MessagePart(otp, QString(), node)
, mMessage(message)
{
mMetaData.isEncrypted = false;
mMetaData.isEncapsulatedRfc822Message = true;
if (!mMessage) {
qCWarning(MIMETREEPARSER_CORE_LOG) << "Node is of type message/rfc822 but doesn't have a message!";
return;
}
parseInternal(message.data());
}
QString EncapsulatedRfc822MessagePart::text() const
{
return renderInternalText();
}
QString EncapsulatedRfc822MessagePart::from() const
{
if (auto from = mMessage->from(false)) {
return from->asUnicodeString();
}
return {};
}
QDateTime EncapsulatedRfc822MessagePart::date() const
{
if (auto date = mMessage->date(false)) {
return date->dateTime();
}
return {};
}
HeadersPart::HeadersPart(ObjectTreeParser *otp, KMime::Content *node)
: MessagePart(otp, QString(), node)
{
}
#include "moc_messagepart.cpp"
diff --git a/src/core/objecttreeparser.cpp b/src/core/objecttreeparser.cpp
index 949f325..893fc48 100644
--- a/src/core/objecttreeparser.cpp
+++ b/src/core/objecttreeparser.cpp
@@ -1,491 +1,491 @@
// This file is part of KMail, the KDE mail client.
// SPDX-FileCopyrightText: 2003 Marc Mutz <mutz@kde.org>
// SPDX-FileCopyrightText: 2002-2004 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
// SPDX-FileCopyrightText: 2009 Andras Mantia <andras@kdab.net>
// SPDX-FileCopyrightText: 2015 Sandro Knauß <sknauss@kde.org>
// SPDX-FileCopyrightText: 2017 Christian Mollekopf <mollekopf@kolabsystems.com>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "objecttreeparser.h"
#include "bodypartformatterbasefactory.h"
#include "bodypartformatter.h"
#include <KMime/Message>
#include <QByteArray>
#include <QDebug>
#include <QMimeDatabase>
#include <QRegularExpression>
#include <QStringDecoder>
#include <QTextStream>
#include <QUrl>
using namespace MimeTreeParser;
using namespace Qt::Literals::StringLiterals;
/*
* Collect message parts bottom up.
* Filter to avoid evaluating a subtree.
* Select parts to include it in the result set. Selecting a part in a branch will keep any parent parts from being selected.
*/
static QList<MessagePart::Ptr> collect(MessagePart::Ptr start,
const std::function<bool(const MessagePart::Ptr &)> &evaluateSubtree,
const std::function<bool(const MessagePart::Ptr &)> &select)
{
auto ptr = start.dynamicCast<MessagePart>();
Q_ASSERT(ptr);
MessagePart::List list;
if (evaluateSubtree(ptr)) {
for (const auto &p : ptr->subParts()) {
list << ::collect(p, evaluateSubtree, select);
}
}
// Don't consider this part if we already selected a subpart
if (list.isEmpty()) {
if (select(ptr)) {
list << start;
}
}
return list;
}
QString ObjectTreeParser::plainTextContent()
{
QString content;
if (mParsedPart) {
auto plainParts = ::collect(
mParsedPart,
[](const MessagePart::Ptr &) {
return true;
},
[](const MessagePart::Ptr &part) {
if (part->isAttachment()) {
return false;
}
if (dynamic_cast<MimeTreeParser::TextMessagePart *>(part.data())) {
return true;
}
if (dynamic_cast<MimeTreeParser::AlternativeMessagePart *>(part.data())) {
return true;
}
return false;
});
for (const auto &part : plainParts) {
content += part->text();
}
}
return content;
}
QString ObjectTreeParser::htmlContent()
{
QString content;
if (mParsedPart) {
MessagePart::List contentParts = ::collect(
mParsedPart,
[](const MessagePart::Ptr &) {
return true;
},
[](const MessagePart::Ptr &part) {
if (dynamic_cast<MimeTreeParser::HtmlMessagePart *>(part.data())) {
return true;
}
if (dynamic_cast<MimeTreeParser::AlternativeMessagePart *>(part.data())) {
return true;
}
return false;
});
for (const auto &part : contentParts) {
if (auto p = dynamic_cast<MimeTreeParser::AlternativeMessagePart *>(part.data())) {
content += p->htmlContent();
} else {
content += part->text();
}
}
}
return content;
}
bool ObjectTreeParser::hasEncryptedParts() const
{
bool result = false;
::collect(
mParsedPart,
[](const MessagePart::Ptr &) {
return true;
},
[&result](const MessagePart::Ptr &part) {
if (const auto enc = dynamic_cast<MimeTreeParser::EncryptedMessagePart *>(part.data())) {
result = true;
}
return false;
});
return result;
}
bool ObjectTreeParser::hasSignedParts() const
{
bool result = false;
::collect(
mParsedPart,
[](const MessagePart::Ptr &) {
return true;
},
[&result](const MessagePart::Ptr &part) {
if (const auto enc = dynamic_cast<MimeTreeParser::SignedMessagePart *>(part.data())) {
result = true;
}
return false;
});
return result;
}
static void print(QTextStream &stream, KMime::Content *node, const QString prefix = {})
{
QByteArray mediaType("text");
QByteArray subType("plain");
if (node->contentType(false) && !node->contentType()->mediaType().isEmpty() && !node->contentType()->subType().isEmpty()) {
mediaType = node->contentType()->mediaType();
subType = node->contentType()->subType();
}
stream << prefix << "! " << mediaType << subType << " isAttachment: " << KMime::isAttachment(node) << "\n";
const auto contents = node->contents();
for (const auto nodeContent : contents) {
print(stream, nodeContent, prefix + QLatin1StringView(" "));
}
}
static void print(QTextStream &stream, const MessagePart &messagePart, const QByteArray pre = {})
{
stream << pre << "# " << messagePart.metaObject()->className() << " isAttachment: " << messagePart.isAttachment() << "\n";
const auto subParts = messagePart.subParts();
for (const auto &subPart : subParts) {
print(stream, *subPart, pre + " ");
}
}
QString ObjectTreeParser::structureAsString() const
{
QString string;
QTextStream stream{&string};
if (mTopLevelContent) {
::print(stream, mTopLevelContent);
}
if (mParsedPart) {
::print(stream, *mParsedPart);
}
return string;
}
void ObjectTreeParser::print()
{
qInfo().noquote() << structureAsString();
}
static KMime::Content *find(KMime::Content *node, const std::function<bool(KMime::Content *)> &select)
{
QByteArray mediaType("text");
QByteArray subType("plain");
if (node->contentType(false) && !node->contentType()->mediaType().isEmpty() && !node->contentType()->subType().isEmpty()) {
mediaType = node->contentType()->mediaType();
subType = node->contentType()->subType();
}
if (select(node)) {
return node;
}
const auto contents = node->contents();
for (const auto nodeContent : contents) {
if (const auto content = find(nodeContent, select)) {
return content;
}
}
return nullptr;
}
KMime::Content *ObjectTreeParser::find(const std::function<bool(KMime::Content *)> &select)
{
return ::find(mTopLevelContent, select);
}
MessagePart::List ObjectTreeParser::collectContentParts()
{
return collectContentParts(mParsedPart);
}
MessagePart::List ObjectTreeParser::collectContentParts(MessagePart::Ptr start)
{
return ::collect(
start,
[start](const MessagePart::Ptr &part) {
// Ignore the top-level
if (start.data() == part.data()) {
return true;
}
if (auto encapsulatedPart = part.dynamicCast<MimeTreeParser::EncapsulatedRfc822MessagePart>()) {
return false;
}
return true;
},
[start](const MessagePart::Ptr &part) {
if (const auto attachment = dynamic_cast<MimeTreeParser::AttachmentMessagePart *>(part.data())) {
return attachment->mimeType() == "text/calendar"_ba;
} else if (const auto text = dynamic_cast<MimeTreeParser::TextMessagePart *>(part.data())) {
auto enc = dynamic_cast<MimeTreeParser::EncryptedMessagePart *>(text->parentPart());
if (enc && enc->error()) {
return false;
}
return true;
} else if (dynamic_cast<MimeTreeParser::AlternativeMessagePart *>(part.data())) {
return true;
} else if (dynamic_cast<MimeTreeParser::HtmlMessagePart *>(part.data())) {
// Don't if we have an alternative part as parent
return true;
} else if (dynamic_cast<MimeTreeParser::EncapsulatedRfc822MessagePart *>(part.data())) {
if (start.data() == part.data()) {
return false;
}
return true;
} else if (const auto enc = dynamic_cast<MimeTreeParser::EncryptedMessagePart *>(part.data())) {
if (enc->error()) {
return true;
}
// If we have a textpart with encrypted and unencrypted subparts we want to return the textpart
if (dynamic_cast<MimeTreeParser::TextMessagePart *>(enc->parentPart())) {
return false;
}
} else if (const auto sig = dynamic_cast<MimeTreeParser::SignedMessagePart *>(part.data())) {
// Signatures without subparts already contain the text
return !sig->hasSubParts();
}
return false;
});
}
MessagePart::List ObjectTreeParser::collectAttachmentParts()
{
MessagePart::List contentParts = ::collect(
mParsedPart,
[](const MessagePart::Ptr &) {
return true;
},
[](const MessagePart::Ptr &part) {
return part->isAttachment();
});
return contentParts;
}
/*
* This naive implementation assumes that there is an encrypted part wrapping a signature.
* For other cases we would have to process both recursively (I think?)
*/
void ObjectTreeParser::decryptAndVerify()
{
// We first decrypt
::collect(
mParsedPart,
[](const MessagePart::Ptr &) {
return true;
},
[](const MessagePart::Ptr &part) {
if (const auto enc = dynamic_cast<MimeTreeParser::EncryptedMessagePart *>(part.data())) {
enc->startDecryption();
}
return false;
});
// And then verify the available signatures
::collect(
mParsedPart,
[](const MessagePart::Ptr &) {
return true;
},
[](const MessagePart::Ptr &part) {
if (const auto enc = dynamic_cast<MimeTreeParser::SignedMessagePart *>(part.data())) {
enc->startVerification();
}
return false;
});
}
QString ObjectTreeParser::resolveCidLinks(const QString &html)
{
auto text = html;
static const auto regex = QRegularExpression(QLatin1StringView("(src)\\s*=\\s*(\"|')(cid:[^\"']+)\\2"));
auto it = regex.globalMatchView(text);
while (it.hasNext()) {
const auto match = it.next();
const auto link = QUrl(match.captured(3));
auto cid = link.path();
auto mailMime = const_cast<KMime::Content *>(find([=](KMime::Content *content) {
if (!content || !content->contentID(false)) {
return false;
}
return QString::fromLatin1(content->contentID(false)->identifier()) == cid;
}));
if (mailMime) {
const auto contentType = mailMime->contentType(false);
if (!contentType) {
qWarning() << "No content type, skipping";
continue;
}
QMimeDatabase mimeDb;
const auto mimetype = mimeDb.mimeTypeForName(QString::fromLatin1(contentType->mimeType())).name();
if (mimetype.startsWith(QLatin1StringView("image/"))) {
// We reencode to base64 below.
- const auto data = mailMime->decodedContent();
+ const auto data = mailMime->decodedBody();
if (data.isEmpty()) {
qWarning() << "Attachment is empty.";
continue;
}
text.replace(match.captured(0), QString::fromLatin1("src=\"data:%1;base64,%2\"").arg(mimetype, QString::fromLatin1(data.toBase64())));
}
} else {
qWarning() << "Failed to find referenced attachment: " << cid;
}
}
return text;
}
//-----------------------------------------------------------------------------
void ObjectTreeParser::parseObjectTree(const QByteArray &mimeMessage)
{
const auto mailData = KMime::CRLFtoLF(mimeMessage);
mMsg = KMime::Message::Ptr(new KMime::Message);
mMsg->setContent(mailData);
mMsg->parse();
// We avoid using mMsg->contentType()->charset(), because that will just return kmime's defaultCharset(), ISO-8859-1
const auto charset = mMsg->contentType()->parameter("charset").toLatin1();
if (charset.isEmpty()) {
mMsg->contentType()->setCharset("us-ascii");
}
parseObjectTree(mMsg.data());
}
void ObjectTreeParser::parseObjectTree(KMime::Content *node)
{
mTopLevelContent = node;
mParsedPart = parseObjectTreeInternal(node, false);
}
MessagePart::Ptr ObjectTreeParser::parsedPart() const
{
return mParsedPart;
}
/*
* This will lookup suitable formatters based on the type,
* and let them generate a list of parts.
* If the formatter generated a list of parts, then those are taken, otherwise we move on to the next match.
*/
MessagePart::List ObjectTreeParser::processType(KMime::Content *node, const QByteArray &mediaType, const QByteArray &subType)
{
static MimeTreeParser::BodyPartFormatterBaseFactory factory;
const auto sub = factory.subtypeRegistry(mediaType.constData());
const auto range = sub.equal_range(subType.constData());
for (auto it = range.first; it != range.second; ++it) {
const auto formatter = it->second;
if (!formatter) {
continue;
}
const auto list = formatter->processList(this, node);
if (!list.isEmpty()) {
return list;
}
}
return {};
}
MessagePart::Ptr ObjectTreeParser::parseObjectTreeInternal(KMime::Content *node, bool onlyOneMimePart)
{
if (!node) {
return MessagePart::Ptr();
}
auto parsedPart = MessagePart::Ptr(new MessagePartList(this, node));
parsedPart->setIsRoot(node->isTopLevel());
const auto contents = node->parent() ? node->parent()->contents() : KMime::Content::List{node};
for (int i = contents.indexOf(node); i < contents.size(); ++i) {
node = contents.at(i);
QByteArray mediaType("text");
QByteArray subType("plain");
if (node->contentType(false) && !node->contentType()->mediaType().isEmpty() && !node->contentType()->subType().isEmpty()) {
mediaType = node->contentType()->mediaType();
subType = node->contentType()->subType();
}
auto messageParts = [&] {
// Try the specific type handler
{
auto list = processType(node, mediaType, subType);
if (!list.isEmpty()) {
return list;
}
}
// Fallback to the generic handler
{
auto list = processType(node, mediaType, "*"_ba);
if (!list.isEmpty()) {
return list;
}
}
// Fallback to the default handler
return defaultHandling(node);
}();
for (const auto &part : messageParts) {
parsedPart->appendSubPart(part);
}
if (onlyOneMimePart) {
break;
}
}
return parsedPart;
}
QList<MessagePart::Ptr> ObjectTreeParser::defaultHandling(KMime::Content *node)
{
if (node->contentType()->mimeType() == "application/octet-stream"_ba
&& (node->contentType()->name().endsWith(QLatin1StringView("p7m")) || node->contentType()->name().endsWith(QLatin1StringView("p7s"))
|| node->contentType()->name().endsWith(QLatin1StringView("p7c")))) {
auto list = processType(node, "application", "pkcs7-mime");
if (!list.isEmpty()) {
return list;
}
}
return {AttachmentMessagePart::Ptr(new AttachmentMessagePart(this, node))};
}
QByteArray ObjectTreeParser::codecNameFor(KMime::Content *node) const
{
if (!node) {
return "UTF-8"_ba;
}
QByteArray charset = node->contentType()->charset().toLower();
// utf-8 is a superset of us-ascii, so we don't lose anything if we use it instead
// utf-8 is used so widely nowadays that it is a good idea to use it to fix issues with broken clients.
if (charset == "us-ascii"_ba) {
charset = "utf-8"_ba;
}
if (!charset.isEmpty()) {
if (const QStringDecoder c(charset.constData()); c.isValid()) {
return charset;
}
}
// no charset means us-ascii (RFC 2045), so using local encoding should
// be okay
return "UTF-8"_ba;
}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Dec 6, 11:57 PM (1 d, 10 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
ee/b3/b5843e52fb37ec683a8733afb35a

Event Timeline