From 3c2300ef50df2e1efe2a77e6d97686d30fde14fc Mon Sep 17 00:00:00 2001 From: "Dirk-Jan C. Binnema" Date: Sun, 16 Aug 2026 16:33:05 +0300 Subject: [PATCH] message/mime-object: cap references Cap the references, since the threading algo is recursive. --- lib/message/mu-mime-object.cc | 30 ++++++++++++++++++------- lib/message/test-mu-message.cc | 40 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/lib/message/mu-mime-object.cc b/lib/message/mu-mime-object.cc index a94b2b9f..7877ebbb 100644 --- a/lib/message/mu-mime-object.cc +++ b/lib/message/mu-mime-object.cc @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -476,16 +477,16 @@ MimeMessage::contacts(Contact::Type ctype) const noexcept * message-ids (in that order). Duplicates are removed. * * The _first_ one in the list determines the thread-id for the message. + * + * The number of references is capped (max_references), since the thread algo is + * recursive, and should still be plenty big. */ std::vector MimeMessage::references() const noexcept { - // is ref already in the list? O(n) but with small n. - auto is_dup = [](auto&& seq, const std::string& ref) { - return std::ranges::any_of(seq, [&](auto&& str) { return ref == str; }); - }; + constexpr size_t max_references = 256; - auto on_blacklist = [](auto&& msgid) { + const auto on_blacklist = [](const char* msgid) { // don't include empty message-ids if (!*msgid) return true; @@ -498,6 +499,7 @@ MimeMessage::references() const noexcept }; std::vector refs; + std::unordered_set seen; // O(1) dup-check, keeps this O(n) for (auto&& ref_header: { "References", "In-reply-to" }) { auto hdr{header(ref_header)}; @@ -508,16 +510,28 @@ MimeMessage::references() const noexcept if (!mime_refs) continue; /* try the next header */ - refs.reserve(refs.size() + g_mime_references_length(mime_refs)); + const auto n_refs{g_mime_references_length(mime_refs)}; + refs.reserve(refs.size() + n_refs); - for (auto i = 0; i != g_mime_references_length(mime_refs); ++i) { + for (auto i = 0; i != n_refs; ++i) { const auto msgid{g_mime_references_get_message_id(mime_refs, i)}; - if (msgid && !is_dup(refs, msgid) && !on_blacklist(msgid)) + if (msgid && !on_blacklist(msgid) && seen.emplace(msgid).second) refs.emplace_back(msgid); } g_mime_references_free(mime_refs); } + // cap hostile/pathological headers: keep the first reference (thread-id) + // plus the most-recent ancestors (the tail). + if (refs.size() > max_references) { + std::vector capped; + capped.reserve(max_references); + capped.emplace_back(std::move(refs.front())); + for (auto it = refs.end() - (max_references - 1); it != refs.end(); ++it) + capped.emplace_back(std::move(*it)); + refs = std::move(capped); + } + return refs; } diff --git a/lib/message/test-mu-message.cc b/lib/message/test-mu-message.cc index f1da499e..dab92173 100644 --- a/lib/message/test-mu-message.cc +++ b/lib/message/test-mu-message.cc @@ -866,6 +866,44 @@ On Thu, Aug 04, 2022 at 05:31:39PM +0100, Robin Murphy wrote: } +static void +test_message_references_capped() +{ + // a too-big References header must not produce an + // unbounded reference list (see MimeMessage::references): keep + // first (thread-id) + most-recent ancestors. + constexpr size_t n_refs = 1000; + constexpr size_t cap = 256; // must match max_references + + std::string hdr{"References:"}; + for (size_t i = 0; i != n_refs; ++i) + hdr += mu_format(" ", i); + + const auto msgtext = + hdr + "\n" + + "To: \"Robin Murphy\" \n" + "From: \"Dan Carpenter\" \n" + "Subject: capped\n" + "Date: Fri, 5 Aug 2022 09:37:02 +0300\n" + "Message-Id: \n" + "\n" + "body\n"; + + auto message{Message::make_from_text( + msgtext, + "/home/test/Maildir/inbox/cur/162342449279256.88888_1.evergrey:2,S")}; + g_assert_true(!!message); + + const auto refs{message->references()}; + g_assert_cmpuint(refs.size(), ==, cap); + // thread-id (first) preserved, and the immediate parent (last) kept. + assert_equal(refs.front(), "r0000@x"); + assert_equal(refs.back(), mu_format("r{:04}@x", n_refs - 1)); + // the middle is dropped: second entry is the start of the kept tail. + assert_equal(refs.at(1), mu_format("r{:04}@x", n_refs - (cap - 1))); +} + + static void test_message_outlook_body() { @@ -1109,6 +1147,8 @@ main(int argc, char* argv[]) test_message_calendar); g_test_add_func("/message/message/references", test_message_references); + g_test_add_func("/message/message/references-capped", + test_message_references_capped); g_test_add_func("/message/message/outlook-body", test_message_outlook_body); g_test_add_func("/message/message/message-id",