From 2a10cebcfb978ce7c5f03a97473eafe9bb3d15dd Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Sun, 2 Aug 2026 17:46:21 +0200 Subject: feat: add HTML builder and cid: scheme handler HtmlBuilder renders parsed messages (and whole threads, as one document, so newsletter threads don't spawn one Chromium process per message) into the HTML string the web view loads. Plain text is escaped and quote lines marked; the cid: rewrite is namespaced per message ("!") so two thread messages sharing a Content-ID don't collide. Hardened namespaceCids beyond the initial sketch after attacking it: handles unquoted cid: attribute values, background=/poster= (not just src/href), and CSS url(cid:...) in both style="" attributes and %2") + .arg(QString::fromUtf8(kStyle), bodyHtml); +} + +QString HtmlBuilder::namespaceCids(const QString &html, const QString &prefix) +{ + if (prefix.isEmpty()) + return html; + + // This runs on the sender's raw, unescaped HTML markup (not on text that + // has been through toHtmlEscaped()), so no double-escaping happens here; + // it is purely a URL rewrite over the existing markup. + // + // Two independent patterns are needed: + // + // 1. Attribute values: src=, href=, background=, poster= (the common + // real-world attributes that can carry a cid: reference in HTML + // email), quoted with " or ', quoted the other way, or entirely + // unquoted ( is valid HTML and unquoted references are + // seen in the wild). Attribute name is matched case-insensitively and + // whitespace/newlines are tolerated around '='. + // + // 2. CSS url(cid:...): appears both inside a style="" attribute value + // and inside a "), + QStringLiteral("m0")); + QVERIFY(html.contains(QStringLiteral("cid:m0!bg@example.org"))); +} + +void TestHtmlBuilder::namespacesMultipleCidRefsOnOneLine() +{ + // Guards against a greedy [^"']+ eating past the first closing quote. + const QString html = HtmlBuilder::namespaceCids( + QStringLiteral(""), + QStringLiteral("m0")); + QVERIFY(html.contains(QStringLiteral("cid:m0!a@x"))); + QVERIFY(html.contains(QStringLiteral("cid:m0!b@x"))); + // A greedy [^"']+ would eat past the first closing quote and swallow the + // second tag's markup into the first cid value; guard against that by + // requiring the first tag to close immediately after its own value. + QVERIFY(html.contains(QStringLiteral("cid:m0!a@x\">"), + QStringLiteral("m0")); + QVERIFY(html.contains(QStringLiteral("cid:m0!logo@example.org"))); +} + +QTEST_MAIN(TestHtmlBuilder) +#include "test_htmlbuilder.moc" -- cgit v1.2.3