[pim/kdepim-addons] plugins/messageviewer/bodypartformatter: Rework the TNEF body part plugin

Allen Winter <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 5f53845281c4af80e1885d873b91684f5780e703 by Allen Winter, on behalf of Volker Krause.
Committed on 09/08/2026 at 09:51.
Pushed by vkrause into branch 'master'.

Rework the TNEF body part plugin

Expand TNEF attachments into message parts and let the corresponding
plugins render them instead of doing that ourselves manually for a few
types.

M  +3    -2    plugins/messageviewer/bodypartformatter/autotests/data/tnef-one-file.mbox.html
M  +6    -4    plugins/messageviewer/bodypartformatter/autotests/data/tnef-two-files.mbox.html
M  +3    -0    plugins/messageviewer/bodypartformatter/autotests/rendertest-common.cpp
M  +0    -4    plugins/messageviewer/bodypartformatter/ms-tnef/CMakeLists.txt
M  +81   -75   plugins/messageviewer/bodypartformatter/ms-tnef/application_ms-tnef.cpp
M  +9    -1    plugins/messageviewer/bodypartformatter/ms-tnef/application_ms-tnef.json

https://invent.kde.org/pim/kdepim-addons/-/commit/5f53845281c4af80e1885d873b91684f5780e703

diff --git a/plugins/messageviewer/bodypartformatter/autotests/data/tnef-one-file.mbox.html b/plugins/messageviewer/bodypartformatter/autotests/data/tnef-one-file.mbox.html
index 92b10d3ca..cf11232a7 100644
--- a/plugins/messageviewer/bodypartformatter/autotests/data/tnef-one-file.mbox.html
+++ b/plugins/messageviewer/bodypartformatter/autotests/data/tnef-one-file.mbox.html
@@ -19,10 +19,11 @@
             </tr>
             <tr class="textAtmB">
               <td>
+                <hr/>
                 <div>
-                  <a href="file:AUTHORS"><img src="file:application-octet-stream.svg" border="0" style="max-width: 100%"/>AUTHORS file for tnef</a>
+                  <a href="attachment:2:e0?place=body"><img align="center" height="48" width="48" src="file:text-x-authors.svg" border="0" style="max-width: 100%" alt=""/>AUTHORS file for tnef</a>
                 </div>
-                <br/>
+                <div/>
               </td>
             </tr>
           </table>
diff --git a/plugins/messageviewer/bodypartformatter/autotests/data/tnef-two-files.mbox.html b/plugins/messageviewer/bodypartformatter/autotests/data/tnef-two-files.mbox.html
index 17063f212..44c326927 100644
--- a/plugins/messageviewer/bodypartformatter/autotests/data/tnef-two-files.mbox.html
+++ b/plugins/messageviewer/bodypartformatter/autotests/data/tnef-two-files.mbox.html
@@ -19,14 +19,16 @@
             </tr>
             <tr class="textAtmB">
               <td>
+                <hr/>
                 <div>
-                  <a href="file:AUTHORS"><img src="file:application-octet-stream.svg" border="0" style="max-width: 100%"/>AUTHORS file for tnef</a>
+                  <a href="attachment:2:e0?place=body"><img align="center" height="48" width="48" src="file:text-x-authors.svg" border="0" style="max-width: 100%" alt=""/>AUTHORS file for tnef</a>
                 </div>
-                <br/>
+                <div/>
+                <hr/>
                 <div>
-                  <a href="file:README"><img src="file:application-octet-stream.svg" border="0" style="max-width: 100%"/>README file for tnef</a>
+                  <a href="attachment:2:e1?place=body"><img align="center" height="48" width="48" src="file:text-x-readme.svg" border="0" style="max-width: 100%" alt=""/>README file for tnef</a>
                 </div>
-                <br/>
+                <div/>
               </td>
             </tr>
           </table>
diff --git a/plugins/messageviewer/bodypartformatter/autotests/rendertest-common.cpp b/plugins/messageviewer/bodypartformatter/autotests/rendertest-common.cpp
index 8932b0e8d..117a4b7e0 100644
--- a/plugins/messageviewer/bodypartformatter/autotests/rendertest-common.cpp
+++ b/plugins/messageviewer/bodypartformatter/autotests/rendertest-common.cpp
@@ -10,6 +10,8 @@
 #include <QRegularExpression>
 #include <QTest>
 
+using namespace Qt::Literals;
+
 static void compareFile(const QString &outFile, const QString &referenceFile)
 {
     QVERIFY(QFile::exists(outFile));
@@ -45,6 +47,7 @@ static void compareFile(const QString &outFile, const QString &referenceFile)
         QString content = QString::fromUtf8(f.readAll());
         f.close();
         content.replace(QRegularExpression(QStringLiteral("\"file:[^\"]*[/(?:%2F)]([^\"/(?:%2F)]*)\"")), QStringLiteral("\"file:\\1\""));
+        content.replace(QRegularExpression(u"src=\"/[^\"]*/([^\"/(?:%2F)]*)\""_s), u"src=\"file:\\1\""_s);
         content.replace(QLatin1StringView("NBSP_ENTITY_PLACEHOLDER"), QLatin1StringView("&nbsp;")); // undo above transformation for xmllint
         content.replace(QRegularExpression(QStringLiteral("/bodypart/\\d+/")), QStringLiteral("/bodypart/0/"));
         QVERIFY(f.open(QIODevice::WriteOnly | QIODevice::Truncate));
diff --git a/plugins/messageviewer/bodypartformatter/ms-tnef/CMakeLists.txt b/plugins/messageviewer/bodypartformatter/ms-tnef/CMakeLists.txt
index d96805405..bfe1015e5 100644
--- a/plugins/messageviewer/bodypartformatter/ms-tnef/CMakeLists.txt
+++ b/plugins/messageviewer/bodypartformatter/ms-tnef/CMakeLists.txt
@@ -14,13 +14,9 @@ ecm_qt_declare_logging_category(messageviewer_bodypartformatter_application_mstn
 target_link_libraries(
     messageviewer_bodypartformatter_application_mstnef
     Qt::Core
-    KF6::CalendarCore
-    KPim6::CalendarUtils
     KPim6::Tnef
     KPim6::MessageCore
     KPim6::MessageViewer
-    KF6::IconThemes
-    KF6::I18n
     KF6::TextTemplate
 )
 install(
diff --git a/plugins/messageviewer/bodypartformatter/ms-tnef/application_ms-tnef.cpp b/plugins/messageviewer/bodypartformatter/ms-tnef/application_ms-tnef.cpp
index 50739dc9a..f17496ce4 100644
--- a/plugins/messageviewer/bodypartformatter/ms-tnef/application_ms-tnef.cpp
+++ b/plugins/messageviewer/bodypartformatter/ms-tnef/application_ms-tnef.cpp
@@ -6,6 +6,7 @@
 
 #include "ms_tnef_debug.h"
 
+#include <MessageViewer/HtmlWriter>
 #include <MessageViewer/MessagePartRenderPlugin>
 #include <MessageViewer/MessagePartRendererBase>
 #include <MessageViewer/MessagePartRendererManager>
@@ -13,17 +14,10 @@
 
 #include <MessageCore/StringUtil>
 
-#include <MessageViewer/HtmlWriter>
+#include <MimeTreeParser/BodyPart>
 #include <MimeTreeParser/MessagePart>
 #include <MimeTreeParser/NodeHelper>
 
-#include <KCalendarCore/Event>
-#include <KCalendarCore/ICalFormat>
-#include <KCalendarCore/Incidence>
-#include <KCalendarCore/MemoryCalendar>
-
-#include <KCalUtils/IncidenceFormatter>
-
 #include <ktnef/formatter.h>
 #include <ktnef/ktnefattach.h>
 #include <ktnef/ktnefmessage.h>
@@ -31,93 +25,99 @@
 
 #include <KTextTemplate/Template>
 
-#include <KIconLoader>
-#include <KLocalizedString>
-
-#include <QFile>
+#include <QBuffer>
 
 namespace
 {
+class TNEFProcessor : public MimeTreeParser::Interface::BodyPartFormatter
+{
+public:
+    MimeTreeParser::MessagePart::Ptr process(MimeTreeParser::Interface::BodyPart &part) const override
+    {
+        KMime::Content *node = part.content();
+        const auto tnefData = node->decodedBody();
+        QBuffer tnefBuffer;
+        tnefBuffer.setData(tnefData);
+
+        KTnef::KTNEFParser parser;
+        if (!parser.openDevice(&tnefBuffer) || !parser.message()) {
+            qCDebug(MS_TNEF_LOG) << "Could not parse TNEF data";
+            return {};
+        }
+
+        auto t = new MimeTreeParser::TextMessagePart(part.objectTreeParser(), node, false);
+        MimeTreeParser::MessagePart::Ptr mp(t);
+        mp->setAttachmentContent(node);
+
+        if (const auto rtf = parser.message()->rtfString(); !rtf.isEmpty()) {
+            mp->appendSubPart(createSubNode(rtf.toUtf8(), "text/rtf", part));
+        }
+        if (const auto ical = KTnef::messageToIcal(parser.message()); !ical.isEmpty()) {
+            mp->appendSubPart(createSubNode(ical.toUtf8(), "text/calendar", part));
+        }
+        if (const auto vcard = KTnef::messageToVcard(parser.message()); !vcard.isEmpty()) {
+            mp->appendSubPart(createSubNode(vcard, "text/vcard", part));
+        }
+
+        const auto attachments = parser.message()->attachmentList();
+        for (const auto &attachment : attachments) {
+            auto attNode = std::make_unique<KMime::Content>();
+            attNode->setBody(QByteArrayView(tnefData).slice(attachment->offset(), attachment->size()).toByteArray());
+            attNode->contentType()->setName(attachment->displayName());
+            attNode->contentType()->setMimeType(attachment->mimeTag().toUtf8());
+            attNode->contentDisposition()->setFilename(attachment->fileName());
+            auto attNodePtr = attNode.get();
+            part.nodeHelper()->attachExtraContent(node, std::move(attNode));
+            mp->appendSubPart(
+                MimeTreeParser::AttachmentMessagePart::Ptr(new MimeTreeParser::AttachmentMessagePart(part.objectTreeParser(), attNodePtr, false)));
+        }
+
+        return mp->subParts().empty() ? nullptr : mp;
+    }
+
+private:
+    static MimeTreeParser::MessagePartPtr createSubNode(const QByteArray &data, const char *mimeType, MimeTreeParser::Interface::BodyPart &parent)
+    {
+        auto node = std::make_unique<KMime::Content>();
+        node->setBody(data);
+        node->contentType()->setMimeType(mimeType);
+        auto nodePtr = node.get();
+        parent.nodeHelper()->attachExtraContent(parent.content(), std::move(node));
+        return MimeTreeParser::AttachmentMessagePart::Ptr(new MimeTreeParser::AttachmentMessagePart(parent.objectTreeParser(), nodePtr, false));
+    }
+};
+
 class Formatter : public MessageViewer::MessagePartRendererBase
 {
 public:
     bool render(const MimeTreeParser::MessagePartPtr &msgPart, MessageViewer::HtmlWriter *htmlWriter, MessageViewer::RenderContext *context) const override
     {
-        Q_UNUSED(context)
-        auto mp = msgPart.dynamicCast<MimeTreeParser::AttachmentMessagePart>();
-        if (!mp || context->isHiddenHint(msgPart)) {
+        auto mp = msgPart.dynamicCast<MimeTreeParser::TextMessagePart>();
+        if (!mp || context->isHiddenHint(msgPart) || !mp->attachmentContent()) {
             return false;
         }
 
-        const QByteArray mimetype = mp->content()->contentType()->mimeType();
+        const QByteArray mimetype = mp->attachmentContent()->contentType()->mimeType();
         if (mimetype != QByteArrayLiteral("application/vnd.ms-tnef") && mimetype != QByteArrayLiteral("application/ms-tnef")) {
             return false;
         }
 
-        const auto fileName = mp->temporaryFilePath();
-        KTnef::KTNEFParser parser;
-        if (!parser.openFile(fileName) || !parser.message()) {
-            qCDebug(MS_TNEF_LOG) << "Could not parse" << fileName;
+        // fall back to the default attachment rendering if there's no sub-parts we managed to extract above
+        if (std::ranges::none_of(msgPart->subParts(), [](const MimeTreeParser::MessagePart::Ptr &part) {
+                return part.dynamicCast<MimeTreeParser::AttachmentMessagePart>() != nullptr;
+            })) {
             return false;
         }
-
-        // Look for an invitation
-        QString inviteStr;
-        if (const auto ical = KTnef::messageToIcal(parser.message()); !ical.isEmpty()) {
-            const KCalendarCore::MemoryCalendar::Ptr cl(new KCalendarCore::MemoryCalendar(QTimeZone::systemTimeZone()));
-            KCalUtils::InvitationFormatterHelper helper;
-            inviteStr = KCalUtils::IncidenceFormatter::formatICalInvitation(ical, cl, &helper);
-            if (inviteStr.isEmpty()) {
-                KCalendarCore::ICalFormat format;
-                if (const auto inc = format.fromString(ical); inc) {
-                    inviteStr = KCalUtils::IncidenceFormatter::extensiveDisplayStr(QString(), inc);
-                }
-            }
-        }
-
         auto c = MessageViewer::MessagePartRendererManager::self()->createContext();
         c.insert(QStringLiteral("block"), msgPart.data());
         c.insert(QStringLiteral("showOnlyOneMimePart"), context->showOnlyOneMimePart());
-        c.insert(QStringLiteral("content"), QVariant::fromValue<MessageViewer::KTextTemplateCallback>([&](KTextTemplate::OutputStream *stream) {
-                     const auto tnefatts = parser.message()->attachmentList();
-                     if (tnefatts.isEmpty() && inviteStr.isEmpty()) {
-                         qCDebug(MS_TNEF_LOG) << "No attachments or invitation found in" << fileName;
-                         (*stream) << QStringLiteral("&nbsp;&lt;") << i18nc("TNEF attachment has no content", "empty") << QStringLiteral("&gt;");
-                         return;
-                     }
-
-                     if (!inviteStr.isEmpty()) {
-                         (*stream) << inviteStr;
-                     }
-
-                     const int numberOfTnef(tnefatts.count());
-                     for (int i = 0; i < numberOfTnef; ++i) {
-                         KTnef::KTNEFAttach *att = tnefatts.at(i);
-                         QString label = att->displayName();
-                         if (label.isEmpty()) {
-                             label = att->name();
-                         }
-                         label = MessageCore::StringUtil::quoteHtmlChars(label, true);
-
-                         const QString dir = mp->nodeHelper()->createTempDir(QLatin1StringView("ktnef-") + QString::number(i));
-                         if (!parser.extractFileTo(att->name(), dir)) {
-                             qCDebug(MS_TNEF_LOG) << "No possible to extract file:" << att->name();
-                         }
-
-                         // falling back to internal TNEF attachment name if no filename is given for the attached file
-                         // this follows the logic of KTNEFParser::extractFileTo(...)
-                         QString attFileName = att->fileName();
-                         if (attFileName.isEmpty()) {
-                             attFileName = att->name();
+        c.insert(QStringLiteral("content"),
+                 QVariant::fromValue<MessageViewer::KTextTemplateCallback>([&]([[maybe_unused]] KTextTemplate::OutputStream *stream) {
+                     const auto parts = msgPart->subParts();
+                     for (const auto &part : parts) {
+                         if (part.dynamicCast<MimeTreeParser::AttachmentMessagePart>()) { // don't render the TNEF data itself
+                             context->renderWithFactory(part, htmlWriter);
                          }
-                         mp->nodeHelper()->addTempFile(dir + QLatin1Char('/') + attFileName);
-                         const QString href = QLatin1StringView("file:") + dir + QLatin1Char('/') + attFileName;
-
-                         const QString iconName =
-                             QUrl::fromLocalFile(MessageViewer::Util::iconPathForMimetype(att->mimeTag(), KIconLoader::Desktop, attFileName)).url();
-
-                         (*stream) << QStringLiteral("<div><a href=\"") << href << QStringLiteral("\"><img src=\"") << iconName
-                                   << QStringLiteral("\" border=\"0\" style=\"max-width: 100%\"/>") << label << QStringLiteral("</a></div><br/>");
                      }
                  }));
 
@@ -128,12 +128,18 @@ public:
     }
 };
 
-class Plugin : public QObject, public MessageViewer::MessagePartRenderPlugin
+class Plugin : public QObject, public MimeTreeParser::Interface::BodyPartFormatterPlugin, public MessageViewer::MessagePartRenderPlugin
 {
     Q_OBJECT
+    Q_INTERFACES(MimeTreeParser::Interface::BodyPartFormatterPlugin)
     Q_INTERFACES(MessageViewer::MessagePartRenderPlugin)
     Q_PLUGIN_METADATA(IID "com.kde.messageviewer.bodypartformatter" FILE "application_ms-tnef.json")
 public:
+    [[nodiscard]] const MimeTreeParser::Interface::BodyPartFormatter *bodyPartFormatter(int idx) const override
+    {
+        return idx < 2 ? new TNEFProcessor() : nullptr;
+    }
+
     MessageViewer::MessagePartRendererBase *renderer(int index) override
     {
         return index == 0 ? new Formatter() : nullptr;
diff --git a/plugins/messageviewer/bodypartformatter/ms-tnef/application_ms-tnef.json b/plugins/messageviewer/bodypartformatter/ms-tnef/application_ms-tnef.json
index db54f30e8..d4a4ddcb9 100644
--- a/plugins/messageviewer/bodypartformatter/ms-tnef/application_ms-tnef.json
+++ b/plugins/messageviewer/bodypartformatter/ms-tnef/application_ms-tnef.json
@@ -1,7 +1,15 @@
 {
+    "formatter": [
+        {
+            "mimetype": "application/vnd.ms-tnef"
+        },
+        {
+            "mimetype": "application/ms-tnef"
+        }
+    ],
     "renderer": [
         {
-            "type": "MimeTreeParser::AttachmentMessagePart"
+            "type": "MimeTreeParser::TextMessagePart"
         }
     ]
 }
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.