Port Spam filter to Rust - part 1
This commit is contained in:
@@ -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";
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
Reference in New Issue
Block a user