/*
* 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;
}
}
${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 ***
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
--- /dev/null
+Return-Path: test@test.com
+From: TEST <test@test.com>
+To: Me <me@me.me>
+Subject: Calendar invite with HTML alternative
+MIME-Version: 1.0
+Date: Mon, 01 Jan 2024 00:00:00 +0000
+Message-ID: <alt-calendar@test.com>
+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
+
+<html>
+<body>
+<p>You are invited to a meeting.</p>
+<p>When: Monday, January 1, 2024</p>
+<p>Where: Conference Room A</p>
+</body>
+</html>
+
+--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--