Page Menu
Home
GnuPG
Search
Configure Global Search
Log In
Files
F34123822
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Size
105 KB
Subscribers
None
View Options
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
Details
Attached
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
Attached To
rMTP MIME Tree Parser
Event Timeline
Log In to Comment