Port Spam filter to Rust - part 1

This commit is contained in:
mdecimus
2024-12-06 18:35:39 +01:00
parent e86a9c1319
commit db7ae48c77
36 changed files with 1010 additions and 545 deletions

View File

@@ -1,19 +0,0 @@
if eval "header.date.exists" {
let "date" "header.date.date";
if eval "date != 0" {
let "date_diff" "env.now - date";
if eval "date_diff > 86400" {
# Older than a day
let "t.DATE_IN_PAST" "1";
} elsif eval "-date_diff > 7200" {
# More than 2 hours in the future
let "t.DATE_IN_FUTURE" "1";
}
} else {
let "t.INVALID_DATE" "1";
}
} else {
let "t.MISSING_DATE" "1";
}

View File

@@ -1,91 +0,0 @@
if eval "env.spf.result == 'pass'" {
let "t.SPF_ALLOW" "1";
} elsif eval "env.spf.result == 'fail'" {
let "t.SPF_FAIL" "1";
} elsif eval "env.spf.result == 'softfail'" {
let "t.SPF_SOFTFAIL" "1";
} elsif eval "env.spf.result == 'neutral'" {
let "t.SPF_NEUTRAL" "1";
} elsif eval "env.spf.result == 'temperror'" {
let "t.SPF_DNSFAIL" "1";
} elsif eval "env.spf.result == 'permerror'" {
let "t.SPF_PERMFAIL" "1";
} else {
let "t.SPF_NA" "1";
}
if eval "env.dkim.result == 'pass'" {
let "t.DKIM_ALLOW" "1";
} elsif eval "env.dkim.result == 'fail'" {
let "t.DKIM_REJECT" "1";
} elsif eval "env.dkim.result == 'temperror'" {
let "t.DKIM_TEMPFAIL" "1";
} elsif eval "env.dkim.result == 'permerror'" {
let "t.DKIM_PERMFAIL" "1";
} else {
let "t.DKIM_NA" "1";
}
if eval "env.arc.result == 'pass'" {
let "t.ARC_ALLOW" "1";
} elsif eval "env.arc.result == 'fail'" {
let "t.ARC_REJECT" "1";
} elsif eval "env.arc.result == 'temperror'" {
let "t.ARC_DNSFAIL" "1";
} elsif eval "env.arc.result == 'permerror'" {
let "t.ARC_INVALID" "1";
} else {
let "t.ARC_NA" "1";
}
if eval "env.dmarc.result == 'pass'" {
let "t.DMARC_POLICY_ALLOW" "1";
} elsif eval "env.dmarc.result == 'temperror'" {
let "t.DMARC_DNSFAIL" "1";
} elsif eval "env.dmarc.result == 'permerror'" {
let "t.DMARC_BAD_POLICY" "1";
} elsif eval "env.dmarc.result == 'fail'" {
if eval "env.dmarc.policy == 'quarantine'" {
let "t.DMARC_POLICY_QUARANTINE" "1";
} elsif eval "env.dmarc.policy == 'reject'" {
let "t.DMARC_POLICY_REJECT" "1";
} else {
let "t.DMARC_POLICY_SOFTFAIL" "1";
}
} else {
let "t.DMARC_NA" "1";
}
if eval "header.DKIM-Signature.exists" {
let "t.DKIM_SIGNED" "1";
if eval "header.ARC-Seal.exists" {
let "t.ARC_SIGNED" "1";
}
}
# Check allowlists
if eval "key_exists('spam-dmarc', from_domain)" {
if eval "t.DMARC_POLICY_ALLOW" {
let "t.ALLOWLIST_DMARC" "1";
} else {
let "t.BLOCKLIST_DMARC" "1";
}
} elsif eval "key_exists('spam-spdk', from_domain)" {
let "is_dkim_pass" "contains(env.dkim.domains, from_domain) || t.ARC_ALLOW";
if eval "is_dkim_pass && t.SPF_ALLOW" {
let "t.ALLOWLIST_SPF_DKIM" "1";
} elsif eval "is_dkim_pass" {
let "t.ALLOWLIST_DKIM" "1";
if eval "!t.SPF_DNSFAIL" {
let "t.BLOCKLIST_SPF" "1";
}
} elsif eval "t.SPF_ALLOW" {
let "t.ALLOWLIST_SPF" "1";
if eval "!t.DKIM_TEMPFAIL" {
let "t.BLOCKLIST_DKIM" "1";
}
} elsif eval "!t.SPF_DNSFAIL && !t.DKIM_TEMPFAIL" {
let "t.BLOCKLIST_SPF_DKIM" "1";
}
}

View File

@@ -1,140 +0,0 @@
# Mailing list scores
let "ml_score" "count(header.List-Id:List-Archive:List-Owner:List-Help:List-Post:X-Loop:List-Subscribe:List-Unsubscribe[*].exists) * 0.125";
if eval "ml_score < 1" {
if eval "header.List-Id.exists" {
let "ml_score" "ml_score + 0.50";
}
if eval "header.List-Subscribe.exists && header.List-Unsubscribe.exists" {
let "ml_score" "ml_score + 0.25";
}
if eval "header.Precedence.exists && (eq_ignore_case(header.Precedence, 'list') || eq_ignore_case(header.Precedence, 'bulk'))" {
let "ml_score" "ml_score + 0.25";
}
}
if eval "ml_score >= 1" {
let "t.MAILLIST" "1";
}
# X-Priority
if eval "header.x-priority.exists" {
let "xp" "header.x-priority";
if eval "xp == 0" {
let "t.HAS_X_PRIO_ZERO" "1";
} elsif eval "xp == 1" {
let "t.HAS_X_PRIO_ONE" "1";
} elsif eval "xp == 2" {
let "t.HAS_X_PRIO_TWO" "1";
} elsif eval "xp <= 4" {
let "t.HAS_X_PRIO_THREE" "1";
} elsif eval "xp >= 5" {
let "t.HAS_X_PRIO_FIVE" "1";
}
}
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)";
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
if eval "header.x-mailer.exists && header.x-mailer.raw_name != 'X-Mailer'" {
let "t.XM_CASE" "1";
}
# Has organization header
if eval "header.organization:organisation.exists" {
let "t.HAS_ORG_HEADER" "1";
}
# Has X-Originating-IP header
if eval "header.X-Originating-IP.exists" {
let "t.HAS_XOIP" "1";
}
# Has List-Unsubscribe header
if eval "header.List-Unsubscribe.exists" {
let "t.HAS_LIST_UNSUB" "1";
}
# Missing version number in X-Mailer or User-Agent headers
if eval "(header.X-Mailer.exists && !has_digits(header.X-Mailer)) || (header.User-Agent.exists && !has_digits(header.User-Agent))" {
let "t.XM_UA_NO_VERSION" "1";
}
# Precedence is bulk
if eval "eq_ignore_case(header.Precedence, 'bulk')" {
let "t.PRECEDENCE_BULK" "1";
}
# Upstream SPAM filtering
if eval "contains_ignore_case(header.X-KLMS-AntiSpam-Status, 'spam')" {
# Kaspersky Security for Mail Server says this message is spam
let "t.KLMS_SPAM" "1";
}
let "spam_hdr" "to_lowercase(header.X-Spam:X-Spam-Flag:X-Spam-Status)";
if eval "contains(spam_hdr, 'yes') || contains(spam_hdr, 'true') || contains(spam_hdr, 'spam')" {
# Message was already marked as spam
let "t.SPAM_FLAG" "1";
}
if eval "contains_ignore_case(header.X-UI-Filterresults:X-UI-Out-Filterresults, 'junk')" {
# United Internet says this message is spam
let "t.UNITEDINTERNET_SPAM" "1";
}
# Compromised hosts
if eval "header.X-PHP-Originating-Script.exists" {
let "t.HAS_X_POS" "1";
if eval "contains(header.X-PHP-Originating-Script, 'eval()')" {
let "t.X_PHP_EVAL" "1";
}
if eval "contains(header.X-PHP-Originating-Script, '../')" {
let "t.HIDDEN_SOURCE_OBJ" "1";
}
}
if eval "header.X-PHP-Script.exists" {
let "t.HAS_X_PHP_SCRIPT" "1";
if eval "contains(header.X-PHP-Script, 'eval()')" {
let "t.X_PHP_EVAL" "1";
}
if eval "contains(header.X-PHP-Script, 'sendmail.php')" {
let "t.PHP_XPS_PATTERN" "1";
}
if eval "contains(header.X-PHP-Script, '../')" {
let "t.HIDDEN_SOURCE_OBJ" "1";
}
}
if eval "contains_ignore_case(header.X-Mailer, 'PHPMailer')" {
let "t.HAS_PHPMAILER_SIG" "1";
}
if eval "header.X-Source:X-Source-Args:X-Source-Dir.exists" {
let "t.HAS_X_SOURCE" "1";
if eval "contains(header.X-Source-Args, '../')" {
let "t.HIDDEN_SOURCE_OBJ" "1";
}
}
if eval "contains(header.X-Authenticated-Sender, ': ')" {
let "t.HAS_X_AS" "1";
}
if eval "contains(header.X-Get-Message-Sender-Via, 'authenticated_id:')" {
let "t.HAS_X_GMSV" "1";
}
if eval "header.X-AntiAbuse.exists" {
let "t.HAS_X_ANTIABUSE" "1";
}
if eval "header.X-Authentication-Warning.exists" {
let "t.HAS_XAW" "1";
}
# Check for empty delimiters in raw headers
let "raw_headers" "header.from:to:cc:subject:reply-to:date[*].raw";
let "i" "count(raw_headers)";
while "i > 0" {
let "i" "i - 1";
if eval "!starts_with(raw_headers[i], ' ')" {
let "t.HEADER_EMPTY_DELIMITER" "1";
break;
}
}

View File

@@ -1,30 +0,0 @@
if eval "!is_ip_addr(env.helo_domain)" {
let "helo" "env.helo_domain";
if eval "contains(helo, '.')" {
if eval "!is_empty(env.iprev.ptr) && !eq_ignore_case(helo, env.iprev.ptr)" {
# Helo does not match reverse IP
let "t.HELO_IPREV_MISMATCH" "1";
}
if eval "!dns_exists(helo, 'ip') && !dns_exists(helo, 'mx')" {
# Helo no resolve to A or MX
let "t.HELO_NORES_A_OR_MX" "1";
}
} else {
if eval "contains(helo, 'user')" {
# HELO contains 'user'
let "t.RCVD_HELO_USER" "1";
}
# Helo not FQDN
let "t.HELO_NOT_FQDN" "1";
}
} else {
# Helo host is bare ip
let "t.HELO_BAREIP" "1";
if eval "env.helo_domain != env.remote_ip" {
# Helo A IP != hostname IP
let "t.HELO_IP_A" "1";
}
}

View File

@@ -1,8 +0,0 @@
# Reverse ip checks
if eval "env.iprev.result != ''" {
if eval "env.iprev.result == 'temperror'" {
let "t.RDNS_DNSFAIL" "1";
} elsif eval "env.iprev.result == 'fail' || env.iprev.result == 'permerror'" {
let "t.RDNS_NONE" "1";
}
}

View File

@@ -1,68 +0,0 @@
let "mid_raw" "trim(header.message-id.raw)";
if eval "!is_empty(mid_raw)" {
let "mid_lcase" "to_lowercase(header.message-id)";
let "mid_rhs" "email_part(mid_lcase, 'domain')";
if eval "!is_empty(mid_rhs)" {
if eval "starts_with(mid_rhs, '[') && ends_with(mid_rhs, ']') && is_ip_addr(strip_suffix(strip_prefix(mid_rhs, '['), ']'))" {
let "t.MID_RHS_IP_LITERAL" "1";
} elsif eval "is_ip_addr(mid_rhs)" {
let "t.MID_BARE_IP" "1";
} elsif eval "!contains(mid_rhs, '.')" {
let "t.MID_RHS_NOT_FQDN" "1";
}
if eval "starts_with(mid_rhs, 'www.')" {
let "t.MID_RHS_WWW" "1";
}
if eval "!is_ascii(mid_raw) || contains(mid_raw, '(') || starts_with(mid_lcase, '@')" {
let "t.INVALID_MSGID" "1";
}
# From address present in Message-ID checks
let "sender" "from_addr";
if eval "is_empty(sender)" {
let "sender" "envelope.from";
}
if eval "!is_empty(sender)" {
if eval "contains(mid_lcase, sender)" {
let "t.MID_CONTAINS_FROM" "1";
} else {
let "from_domain" "email_part(sender, 'domain')";
let "mid_sld" "domain_part(mid_rhs, 'sld')";
if eval "mid_rhs == from_domain" {
let "t.MID_RHS_MATCH_FROM" "1";
} elsif eval "!is_empty(mid_sld) && domain_part(from_domain, 'sld') == mid_sld" {
let "t.MID_RHS_MATCH_FROMTLD" "1";
}
}
}
# To/Cc addresses present in Message-ID checks
let "recipients_len" "count(recipients)";
let "i" "0";
while "i < recipients_len" {
let "rcpt" "recipients[i]";
let "i" "i + 1";
if eval "contains(mid_lcase, rcpt)" {
let "t.MID_CONTAINS_TO" "1";
} elsif eval "email_part(rcpt, 'domain') == mid_rhs" {
let "t.MID_RHS_MATCH_TO" "1";
}
}
} else {
let "t.INVALID_MSGID" "1";
}
if eval "!starts_with(mid_raw, '<') || !contains(mid_raw, '>')" {
let "t.MID_MISSING_BRACKETS" "1";
}
} else {
let "t.MISSING_MID" "1";
}

View File

@@ -1,43 +0,0 @@
# Convert body to plain text
let "text_body" "body.to_text";
# Obtain all URLs in the body
let "body_urls" "tokenize(text_body, 'uri')";
# Obtain all URLs in href and src attributes
let "html_body_urls" "html_attrs(body.html, '', ['href', 'src'])";
# Obtain all URLs in the subject, combine them with all other URLs and remove duplicates
let "urls" "dedup(tokenize(header.subject, 'uri') + body_urls + html_body_urls)";
# Obtain thread name and subject
let "subject_lc" "to_lowercase(header.subject)";
let "subject_clean" "thread_name(header.subject)";
let "body_and_subject" "subject_clean + ' ' + text_body";
# Obtain all recipients
let "recipients" "to_lowercase(header.to:cc:bcc[*].addr[*])";
let "recipients_clean" "winnow(dedup(recipients))";
let "recipients_to" "header.to[*].addr[*]";
let "recipients_cc" "header.cc[*].addr[*]";
# Obtain From parts
let "from_name" "to_lowercase(trim(header.from.name))";
let "from_addr" "to_lowercase(trim(header.from.addr))";
let "from_local" "email_part(from_addr, 'local')";
let "from_domain" "email_part(from_addr, 'domain')";
let "from_domain_sld" "domain_part(from_domain, 'sld')";
# Obtain Reply-To address
let "rto_addr" "to_lowercase(header.reply-to.addr)";
# Obtain Envelope From parts
let "envfrom_local" "email_part(envelope.from, 'local')";
let "envfrom_domain" "email_part(envelope.from, 'domain')";
let "envfrom_domain_sld" "domain_part(envfrom_domain, 'sld')";
# Obtain HELO domain SLD
let "helo_domain_sld" "domain_part(env.helo_domain, 'sld')";
# Create score variable
let "score" "0.0";