From 795ad975533c60ea53f14816cdcc3c6f5a4f11e1 Mon Sep 17 00:00:00 2001 From: mdecimus Date: Fri, 27 Oct 2023 11:02:51 +0200 Subject: [PATCH] Imported empty headers/parts rules from Rspamd --- CHANGELOG.md | 10 ++ crates/smtp/src/scripts/functions/header.rs | 8 ++ crates/smtp/src/scripts/functions/mod.rs | 1 + resources/config/spamfilter/maps/scores.map | 4 + .../spamfilter/scripts/composites.sieve | 4 + .../config/spamfilter/scripts/headers.sieve | 5 +- .../config/spamfilter/scripts/mime.sieve | 99 +++++++++++-------- tests/resources/smtp/antispam/headers.test | 6 ++ tests/resources/smtp/antispam/mime.test | 24 ++--- tests/src/smtp/inbound/antispam.rs | 7 +- 10 files changed, 115 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 326bc7eb1..726a9174a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [0.4.1] - 2023-10-26 + +## Added + +### Changed + +### Fixed +- Dockerfile entrypoint script. +- `bayes_is_balanced` function. + ## [0.4.0] - 2023-10-25 This version introduces some breaking changes in the configuration file. Please read the [UPGRADING.md](UPGRADING.md) file for more information. diff --git a/crates/smtp/src/scripts/functions/header.rs b/crates/smtp/src/scripts/functions/header.rs index 84180f76b..5ee773cc2 100644 --- a/crates/smtp/src/scripts/functions/header.rs +++ b/crates/smtp/src/scripts/functions/header.rs @@ -75,6 +75,14 @@ pub fn fn_attachment_name<'x>(ctx: &'x Context<'x, SieveContext>, _: Vec(ctx: &'x Context<'x, SieveContext>, _: Vec) -> Variable { + ctx.message() + .part(ctx.part()) + .map(|p| p.len()) + .unwrap_or_default() + .into() +} + pub fn fn_thread_name<'x>(_: &'x Context<'x, SieveContext>, v: Vec) -> Variable { v[0].transform(|s| thread_name(s).into()) } diff --git a/crates/smtp/src/scripts/functions/mod.rs b/crates/smtp/src/scripts/functions/mod.rs index 6ac994709..5a00ad436 100644 --- a/crates/smtp/src/scripts/functions/mod.rs +++ b/crates/smtp/src/scripts/functions/mod.rs @@ -110,6 +110,7 @@ pub fn register_functions() -> FunctionMap { .with_function_no_args("is_body", fn_is_body) .with_function_no_args("var_names", fn_is_var_names) .with_function_no_args("attachment_name", fn_attachment_name) + .with_function_no_args("mime_part_len", fn_mime_part_len) } pub trait ApplyString<'x> { diff --git a/resources/config/spamfilter/maps/scores.map b/resources/config/spamfilter/maps/scores.map index 94efcbf7f..8d47d6f46 100644 --- a/resources/config/spamfilter/maps/scores.map +++ b/resources/config/spamfilter/maps/scores.map @@ -358,3 +358,7 @@ XM_CASE 0.5 XM_UA_NO_VERSION 0.01 X_PHP_EVAL 4.0 ZERO_WIDTH_SPACE_URL 7.0 +SHORT_PART_BAD_HEADERS 7.0 +MISSING_ESSENTIAL_HEADERS 7.0 +SINGLE_SHORT_PART 0.0 +COMPLETELY_EMPTY 7.0 diff --git a/resources/config/spamfilter/scripts/composites.sieve b/resources/config/spamfilter/scripts/composites.sieve index 073a39880..27bffa698 100644 --- a/resources/config/spamfilter/scripts/composites.sieve +++ b/resources/config/spamfilter/scripts/composites.sieve @@ -1,3 +1,7 @@ +if eval "t.MISSING_ESSENTIAL_HEADERS && t.SINGLE_SHORT_PART" { + let "t.SHORT_PART_BAD_HEADERS" "1"; +} + if eval "t.FORGED_RECIPIENTS && t.MAILLIST" { let "t.FORGED_RECIPIENTS_MAILLIST" "1"; } diff --git a/resources/config/spamfilter/scripts/headers.sieve b/resources/config/spamfilter/scripts/headers.sieve index b4ca35e48..81d4ea67b 100644 --- a/resources/config/spamfilter/scripts/headers.sieve +++ b/resources/config/spamfilter/scripts/headers.sieve @@ -32,8 +32,11 @@ if eval "header.x-priority.exists" { } let "unique_header_names" "to_lowercase(header.Content-Type:Content-Transfer-Encoding:Date:From:Sender:Reply-To:To:Cc:Bcc:Message-ID:In-Reply-To:References:Subject[*].raw_name)"; -if eval "count(unique_header_names) != count(dedup(unique_header_names))" { +let "unique_header_names_len" "count(unique_header_names)"; +if eval "unique_header_names_len != count(dedup(unique_header_names))" { let "t.MULTIPLE_UNIQUE_HEADERS" "1"; +} elsif eval "unique_header_names_len == 0" { + let "t.MISSING_ESSENTIAL_HEADERS" "1"; } # Wrong case X-Mailer diff --git a/resources/config/spamfilter/scripts/mime.sieve b/resources/config/spamfilter/scripts/mime.sieve index acc9e697f..87c9f9eb0 100644 --- a/resources/config/spamfilter/scripts/mime.sieve +++ b/resources/config/spamfilter/scripts/mime.sieve @@ -8,6 +8,8 @@ if eval "!header.mime-version.exists" { let "has_text_part" "0"; let "is_encrypted" "0"; +let "parts_num" "0"; +let "parts_max_len" "0"; if eval "header.Content-Type.exists && !header.Content-Disposition:Content-Transfer-Encoding:MIME-Version.exists && !eq_ignore_case(header.Content-Type, 'text/plain')" { # Only Content-Type header without other MIME headers @@ -97,49 +99,60 @@ foreverypart { } elsif eval "subtype == 'encrypted'" { set "is_encrypted" "1"; } - } elsif eval "type == 'text'" { - # MIME text part claims to be ASCII but isn't - if eval "cte == '' || cte == '7bit'" { - if eval "!is_ascii(part.raw)" { - let "t.R_BAD_CTE_7BIT" "1"; - } - } else { - if eval "cte == 'base64'" { - if eval "is_ascii(part.text)" { - # Has text part encoded in base64 that does not contain any 8bit characters - let "t.MIME_BASE64_TEXT_BOGUS" "1"; - } else { - # Has text part encoded in base64 - let "t.MIME_BASE64_TEXT" "1"; + } else { + if eval "type == 'text'" { + # MIME text part claims to be ASCII but isn't + if eval "cte == '' || cte == '7bit'" { + if eval "!is_ascii(part.raw)" { + let "t.R_BAD_CTE_7BIT" "1"; + } + } else { + if eval "cte == 'base64'" { + if eval "is_ascii(part.text)" { + # Has text part encoded in base64 that does not contain any 8bit characters + let "t.MIME_BASE64_TEXT_BOGUS" "1"; + } else { + # Has text part encoded in base64 + let "t.MIME_BASE64_TEXT" "1"; + } } - } - if eval "subtype == 'plain' && is_empty(header.content-type.attr.charset)" { - # Charset header is missing - let "t.R_MISSING_CHARSET" "1"; + if eval "subtype == 'plain' && is_empty(header.content-type.attr.charset)" { + # Charset header is missing + let "t.R_MISSING_CHARSET" "1"; + } + } + let "has_text_part" "1"; + } elsif eval "type == 'application'" { + if eval "subtype == 'pkcs7-mime'" { + let "t.ENCRYPTED_SMIME" "1"; + let "part_is_attachment" "0"; + } elsif eval "subtype == 'pkcs7-signature'" { + let "t.SIGNED_SMIME" "1"; + let "part_is_attachment" "0"; + } elsif eval "subtype == 'pgp-encrypted'" { + let "t.ENCRYPTED_PGP" "1"; + let "part_is_attachment" "0"; + } elsif eval "subtype == 'pgp-signature'" { + let "t.SIGNED_PGP" "1"; + let "part_is_attachment" "0"; + } elsif eval "subtype == 'octet-stream'" { + if eval "!is_encrypted && + !header.content-id.exists && + (!header.content-disposition.exists || + (!eq_ignore_case(header.content-disposition.type, 'attachment') && + is_empty(header.content-disposition.attr.filename)))" { + let "t.CTYPE_MISSING_DISPOSITION" "1"; + } } } - let "has_text_part" "1"; - } elsif eval "type == 'application'" { - if eval "subtype == 'pkcs7-mime'" { - let "t.ENCRYPTED_SMIME" "1"; - let "part_is_attachment" "0"; - } elsif eval "subtype == 'pkcs7-signature'" { - let "t.SIGNED_SMIME" "1"; - let "part_is_attachment" "0"; - } elsif eval "subtype == 'pgp-encrypted'" { - let "t.ENCRYPTED_PGP" "1"; - let "part_is_attachment" "0"; - } elsif eval "subtype == 'pgp-signature'" { - let "t.SIGNED_PGP" "1"; - let "part_is_attachment" "0"; - } elsif eval "subtype == 'octet-stream'" { - if eval "!is_encrypted && - !header.content-id.exists && - (!header.content-disposition.exists || - (!eq_ignore_case(header.content-disposition.type, 'attachment') && - is_empty(header.content-disposition.attr.filename)))" { - let "t.CTYPE_MISSING_DISPOSITION" "1"; + + # Increase part count + let "parts_num" "parts_num + 1"; + if eval "parts_num == 1" { + let "parts_len" "mime_part_len()"; + if eval "parts_len > parts_max_len" { + let "parts_max_len" "parts_len"; } } } @@ -201,10 +214,18 @@ foreverypart { } +# Message contains both text and encrypted parts if eval "has_text_part && (t.ENCRYPTED_SMIME || t.SIGNED_SMIME || t.ENCRYPTED_PGP || t.SIGNED_PGP)" { let "t.BOGUS_ENCRYPTED_AND_TEXT" "1"; } +# Message contains only one short part +if eval "parts_num == 1 && parts_max_len < 64" { + let "t.SINGLE_SHORT_PART" "1"; +} elsif eval "parts_max_len == 0" { + let "t.COMPLETELY_EMPTY" "1"; +} + # Check for mixed script in body if eval "!is_single_script(text_body)" { let "t.R_MIXED_CHARSET" "1"; diff --git a/tests/resources/smtp/antispam/headers.test b/tests/resources/smtp/antispam/headers.test index c496aac15..54eaa8165 100644 --- a/tests/resources/smtp/antispam/headers.test +++ b/tests/resources/smtp/antispam/headers.test @@ -77,3 +77,9 @@ List-Unsubscribe: 1 Subject: test Test + +expect MISSING_ESSENTIAL_HEADERS + +X-Other: test + +Test diff --git a/tests/resources/smtp/antispam/mime.test b/tests/resources/smtp/antispam/mime.test index c141cbe87..71a639523 100644 --- a/tests/resources/smtp/antispam/mime.test +++ b/tests/resources/smtp/antispam/mime.test @@ -1,17 +1,17 @@ -expect MISSING_MIME_VERSION +expect MISSING_MIME_VERSION SINGLE_SHORT_PART Content-Type: text/plain; charset="us-ascii" Test -expect MV_CASE +expect MV_CASE SINGLE_SHORT_PART Content-Type: text/plain; charset="us-ascii" Mime-Version: 1.0 Test -expect CTE_CASE CT_EXTRA_SEMI +expect CTE_CASE CT_EXTRA_SEMI SINGLE_SHORT_PART Content-Type: text/plain; charset="us-ascii"; Content-Transfer-Encoding: 7Bit @@ -19,7 +19,7 @@ MIME-Version: 1.0 Test -expect BROKEN_CONTENT_TYPE +expect BROKEN_CONTENT_TYPE SINGLE_SHORT_PART Content-Type: ; tag=1 Content-Transfer-Encoding: 7bit @@ -27,13 +27,13 @@ MIME-Version: 1.0 Test -expect MIME_HEADER_CTYPE_ONLY MISSING_MIME_VERSION +expect MIME_HEADER_CTYPE_ONLY MISSING_MIME_VERSION SINGLE_SHORT_PART Content-Type: text/html; charset="us-ascii" Test -expect R_BAD_CTE_7BIT +expect R_BAD_CTE_7BIT SINGLE_SHORT_PART Content-Type: text/plain Content-Transfer-Encoding: 7bit @@ -41,7 +41,7 @@ MIME-Version: 1.0 Téstíng -expect R_MISSING_CHARSET +expect R_MISSING_CHARSET SINGLE_SHORT_PART Content-Type: text/plain Content-Transfer-Encoding: 8bit @@ -49,7 +49,7 @@ MIME-Version: 1.0 Test -expect MIME_BASE64_TEXT_BOGUS +expect MIME_BASE64_TEXT_BOGUS SINGLE_SHORT_PART Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: base64 @@ -58,7 +58,7 @@ MIME-Version: 1.0 aGVsbG8gd29ybGQK -expect MIME_BASE64_TEXT +expect MIME_BASE64_TEXT SINGLE_SHORT_PART Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: base64 @@ -281,7 +281,7 @@ Content-Transfer-Encoding: 7bit --boundary-- -expect CTYPE_MISSING_DISPOSITION HAS_ATTACHMENT +expect CTYPE_MISSING_DISPOSITION HAS_ATTACHMENT SINGLE_SHORT_PART Content-Type: application/octet-stream MIME-Version: 1.0 @@ -326,7 +326,7 @@ this is a test --boundary-- -expect CTYPE_MISSING_DISPOSITION HAS_ATTACHMENT +expect CTYPE_MISSING_DISPOSITION HAS_ATTACHMENT SINGLE_SHORT_PART Content-Type: application/octet-stream MIME-Version: 1.0 @@ -353,7 +353,7 @@ this is a test --boundary-- -expect R_MIXED_CHARSET +expect R_MIXED_CHARSET SINGLE_SHORT_PART Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: 8bit diff --git a/tests/src/smtp/inbound/antispam.rs b/tests/src/smtp/inbound/antispam.rs index 71687422e..b6654fd28 100644 --- a/tests/src/smtp/inbound/antispam.rs +++ b/tests/src/smtp/inbound/antispam.rs @@ -212,7 +212,12 @@ async fn antispam() { ) .replace("%CFG_PATH%", base_path.as_path().to_str().unwrap()); let base_path = base_path.join("scripts"); - let script_config = fs::read_to_string(base_path.join("config.sieve")).unwrap(); + let script_config = fs::read_to_string(base_path.join("config.sieve")) + .unwrap() + .replace( + "AUTOLEARN_SPAM_HAM_BALANCE\" \"0.9", + "AUTOLEARN_SPAM_HAM_BALANCE\" \"0.0", + ); let script_prelude = fs::read_to_string(base_path.join("prelude.sieve")).unwrap(); let mut all_scripts = script_config.clone() + "\n" + script_prelude.as_str(); for test_name in tests {