From: Vsevolod Stakhov Date: Thu, 19 Feb 2026 10:31:07 +0000 (+0000) Subject: [Fix] Check MIME structure for alternatives in has_only_html_part X-Git-Tag: 4.0.0~87 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=a2e8d962aa3e32aaab37c244918be7c30fda768e;p=thirdparty%2Frspamd.git [Fix] Check MIME structure for alternatives in has_only_html_part has_only_html_part() relied solely on alt_text_part to find alternatives, but parts like text/calendar (detected as ics with no_text=true) are not in text_parts and thus invisible to the search. Now we also walk the MIME tree: if the HTML part sits inside a multipart/alternative with other siblings, it has a structural alternative and MIME_HTML_ONLY should not fire. --- diff --git a/src/libmime/mime_expressions.c b/src/libmime/mime_expressions.c index d62c28735b..061210d1ef 100644 --- a/src/libmime/mime_expressions.c +++ b/src/libmime/mime_expressions.c @@ -1474,16 +1474,41 @@ rspamd_has_only_html_part(struct rspamd_task *task, GArray *args, /* * Return TRUE if there's any HTML part (not attachment) that has no - * text/plain alternative in its multipart/alternative parent. - * This properly handles nested structures like: - * - multipart/mixed → multipart/related → text/html (no text/plain) - * - multipart/alternative → text/plain + multipart/related → text/html + * alternative in its multipart/alternative parent. + * + * We check two things: + * 1. alt_text_part: a text/plain sibling found in text_parts + * 2. MIME structure: any sibling in a multipart/alternative parent + * (covers non-text alternatives like text/calendar that may not + * be in text_parts due to no_text detection flag) */ PTR_ARRAY_FOREACH(MESSAGE_FIELD(task, text_parts), i, p) { if (!IS_TEXT_PART_ATTACHMENT(p) && IS_TEXT_PART_HTML(p)) { - if (p->alt_text_part == NULL) { - /* HTML part with no text alternative */ + if (p->alt_text_part != NULL) { + /* Has a text/plain alternative */ + continue; + } + + /* Check MIME structure: walk up to find multipart/alternative */ + struct rspamd_mime_part *parent = p->mime_part->parent_part; + gboolean has_structural_alt = FALSE; + rspamd_ftok_t alt_tok = {.begin = "alternative", .len = 11}; + + while (parent) { + if (IS_PART_MULTIPART(parent) && parent->ct && + rspamd_ftok_casecmp(&parent->ct->subtype, &alt_tok) == 0) { + /* Found a multipart/alternative ancestor */ + if (parent->specific.mp && parent->specific.mp->children && + parent->specific.mp->children->len > 1) { + has_structural_alt = TRUE; + } + break; + } + parent = parent->parent_part; + } + + if (!has_structural_alt) { return TRUE; } } diff --git a/test/functional/cases/001_merged/100_general.robot b/test/functional/cases/001_merged/100_general.robot index 4552f41674..a6e6191ea9 100644 --- a/test/functional/cases/001_merged/100_general.robot +++ b/test/functional/cases/001_merged/100_general.robot @@ -10,6 +10,7 @@ ${MIXED_RELATED_HTML} ${RSPAMD_TESTDIR}/messages/mixed-related-html-only.eml ${ALT_NESTED_RFC822} ${RSPAMD_TESTDIR}/messages/alternative-nested-rfc822.eml ${ALT_EMPTY_RELATED} ${RSPAMD_TESTDIR}/messages/alternative-empty-related.eml ${MIXED_HTML_ZIP} ${RSPAMD_TESTDIR}/messages/mixed-html-zip.eml +${ALT_HTML_CALENDAR} ${RSPAMD_TESTDIR}/messages/alt-html-calendar.eml ${SETTINGS_NOSYMBOLS} {symbols_enabled = []} *** Test Cases *** @@ -94,3 +95,8 @@ HTML ONLY - multipart/mixed with html and non-text attachment Scan File ${MIXED_HTML_ZIP} ... Settings={symbols_enabled = [MIME_HTML_ONLY]} Expect Symbol MIME_HTML_ONLY + +HTML ONLY - multipart/alternative with html and text/calendar + Scan File ${ALT_HTML_CALENDAR} + ... Settings={symbols_enabled = [MIME_HTML_ONLY]} + Do Not Expect Symbol MIME_HTML_ONLY diff --git a/test/functional/messages/alt-html-calendar.eml b/test/functional/messages/alt-html-calendar.eml new file mode 100644 index 0000000000..1143855069 --- /dev/null +++ b/test/functional/messages/alt-html-calendar.eml @@ -0,0 +1,61 @@ +Return-Path: test@test.com +From: TEST +To: Me +Subject: Calendar invite with HTML alternative +MIME-Version: 1.0 +Date: Mon, 01 Jan 2024 00:00:00 +0000 +Message-ID: +Content-Type: multipart/mixed; boundary="outer-boundary" + +--outer-boundary +Content-Type: multipart/alternative; boundary="inner-boundary" + +--inner-boundary +Content-Type: text/html; charset=utf-8 + + + +

You are invited to a meeting.

+

When: Monday, January 1, 2024

+

Where: Conference Room A

+ + + +--inner-boundary +Content-Type: text/calendar; charset=utf-8; method=REQUEST + +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Test//Test//EN +METHOD:REQUEST +BEGIN:VEVENT +DTSTART:20240101T100000Z +DTEND:20240101T110000Z +SUMMARY:Team Meeting +LOCATION:Conference Room A +ORGANIZER:mailto:test@test.com +ATTENDEE:mailto:me@me.me +END:VEVENT +END:VCALENDAR + +--inner-boundary-- + +--outer-boundary +Content-Type: application/ics; name="invite.ics" +Content-Disposition: attachment; filename="invite.ics" + +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Test//Test//EN +METHOD:REQUEST +BEGIN:VEVENT +DTSTART:20240101T100000Z +DTEND:20240101T110000Z +SUMMARY:Team Meeting +LOCATION:Conference Room A +ORGANIZER:mailto:test@test.com +ATTENDEE:mailto:me@me.me +END:VEVENT +END:VCALENDAR + +--outer-boundary--