Port Spam filter to Rust - part 3

This commit is contained in:
mdecimus
2024-12-09 17:49:11 +01:00
parent 4453dc8f3d
commit f0d84c8e68
34 changed files with 1791 additions and 653 deletions

View File

@@ -1,49 +0,0 @@
if eval "(contains(subject_lc, 'delivery') &&
(contains(subject_lc, 'failed') ||
contains(subject_lc, 'report') ||
contains(subject_lc, 'status') ||
contains(subject_lc, 'warning'))) ||
(contains(subject_lc, 'failure') &&
(contains(subject_lc, 'delivery') ||
contains(subject_lc, 'notice') ||
contains(subject_lc, 'mail') )) ||
(contains(subject_lc, 'delivered') &&
(contains(subject_lc, 'couldn\\'t be') ||
contains(subject_lc, 'could not be') ||
contains(subject_lc, 'hasn\\'t been') ||
contains(subject_lc, 'has not been'))) ||
contains(subject_lc, 'returned mail') ||
contains(subject_lc, 'undeliverable') ||
contains(subject_lc, 'undelivered')" {
# Subject contains words or phrases typical for DSN
let "t.SUBJ_BOUNCE_WORDS" "1";
}
if eval "is_empty(envelope.from)" {
if eval "eq_ignore_case(header.content-type, 'multipart/report') &&
( eq_ignore_case(header.content-type.attr.report-type, 'delivery-status') ||
eq_ignore_case(header.content-type.attr.report-type, 'disposition-notification'))" {
let "t.BOUNCE" "1";
} else {
let "from" "to_lowercase(header.from)";
if eval "contains(from, 'mdaemon') && !is_empty(header.X-MDDSN-Message)" {
let "t.BOUNCE" "1";
} elsif eval "contains(from, 'postmaster') || contains(from, 'mailer-daemon')" {
if eval "t.SUBJ_BOUNCE_WORDS" {
let "t.BOUNCE" "1";
} else {
foreverypart {
if eval "(eq_ignore_case(header.content-type.type, 'message') ||
eq_ignore_case(header.content-type.type, 'text')) &&
(eq_ignore_case(header.content-type.subtype, 'rfc822-headers') ||
eq_ignore_case(header.content-type.subtype, 'rfc822'))" {
let "t.BOUNCE" "1";
break;
}
}
}
}
}
}

View File

@@ -1,93 +0,0 @@
let "rcvd_raw" "header.received[*].raw";
let "rcvd_count" "count(rcvd_raw)";
# Count received headers
if eval "rcvd_count == 0" {
let "t.RCVD_COUNT_ZERO" "1";
} elsif eval "rcvd_count == 1" {
let "t.RCVD_COUNT_ONE" "1";
} elsif eval "rcvd_count == 2" {
let "t.RCVD_COUNT_TWO" "1";
} elsif eval "rcvd_count == 3" {
let "t.RCVD_COUNT_THREE" "1";
} elsif eval "rcvd_count <= 5" {
let "t.RCVD_COUNT_FIVE" "1";
} elsif eval "rcvd_count <= 7" {
let "t.RCVD_COUNT_SEVEN" "1";
} elsif eval "rcvd_count <= 12" {
let "t.RCVD_COUNT_TWELVE" "1";
}
# Received from an authenticated user
if eval "!is_empty(env.authenticated_as)" {
let "t.RCVD_VIA_SMTP_AUTH" "1";
}
# Received headers have non-ASCII characters
if eval "!is_ascii(rcvd_raw)" {
let "t.RCVD_ILLEGAL_CHARS" "1";
}
let "i" "0";
let "tls_count" "0";
let "rcvd_from_ip" "0";
while "i < rcvd_count" {
let "i" "i + 1";
let "helo_domain" "received_part(i, 'from')";
# Check for a forged received trail
if eval "!t.FORGED_RCVD_TRAIL" {
let "iprev" "received_part(i, 'iprev')";
if eval "!is_empty(iprev) && !is_empty(helo_domain) && !eq_ignore_case(helo_domain, iprev)" {
let "t.FORGED_RCVD_TRAIL" "1";
}
}
if eval "!t.PREVIOUSLY_DELIVERED" {
let "for" "received_part(i, 'for')";
# Recipient appears on Received trail
if eval "!is_empty(for) && contains_ignore_case(recipients, for)" {
let "t.PREVIOUSLY_DELIVERED" "1";
}
}
if eval "!t.RCVD_HELO_USER && eq_ignore_case(helo_domain, 'user')" {
# Received: HELO contains 'user'
let "t.RCVD_HELO_USER" "1";
}
if eval "!is_empty(received_part(i, 'from.ip'))" {
# Received from an IP address rather than a FQDN
let "rcvd_from_ip" "rcvd_from_ip + 1";
}
if eval "!is_empty(received_part(i, 'tls'))" {
# Received with TLS
let "tls_count" "tls_count + 1";
}
}
if eval "rcvd_from_ip >= 2 || (rcvd_from_ip == 1 && is_ip_addr(env.helo_domain))" {
# Has two or more Received headers containing bare IP addresses
let "t.RCVD_DOUBLE_IP_SPAM" "1";
}
if eval "rcvd_count == 0" {
# One received header in a message (currently zero but one header will be added later by the MTA)
let "t.ONCE_RECEIVED" "1";
# Message has been directly delivered from MUA to local MX
if eval "header.User-Agent.exists || header.X-Mailer.exists" {
let "t.DIRECT_TO_MX" "1";
}
}
# Received with TLS checks
if eval "rcvd_count > 0 && tls_count == rcvd_count && !is_empty(env.tls.version)" {
let "t.RCVD_TLS_ALL" "1";
} elsif eval "!is_empty(env.tls.version)" {
let "t.RCVD_TLS_LAST" "1";
} else {
let "t.RCVD_NO_TLS_LAST" "1";
}

View File

@@ -1,78 +0,0 @@
let "raw_subject_lc" "to_lowercase(header.subject.raw)";
let "is_ascii_subject" "is_ascii(subject_lc)";
if eval "len(subject_clean) >= 10 && count(tokenize(subject_clean, 'words')) > 1 && is_uppercase(subject_clean)" {
# Subject contains mostly capital letters
let "t.SUBJ_ALL_CAPS" "1";
}
if eval "count_chars(subject_clean) > 200" {
# Subject is very long
let "t.LONG_SUBJ" "1";
}
if eval "!is_empty(tokenize(subject_lc, 'uri_strict'))" {
# Subject contains a URL
let "t.URL_IN_SUBJECT" "1";
}
if eval "!is_ascii(raw_subject_lc) && !env.param.smtputf8 && env.param.body != '8bitmime' && env.param.body != 'binarymime'" {
# Subject needs encoding
let "t.SUBJECT_NEEDS_ENCODING" "1";
}
if eval "!header.Subject.exists" {
# Missing subject header
let "t.MISSING_SUBJECT" "1";
} elsif eval "is_empty(trim(subject_lc))" {
# Subject is empty
let "t.EMPTY_SUBJECT" "1";
}
if eval "is_ascii(subject_lc) && contains(raw_subject_lc, '=?') && contains(raw_subject_lc, '?=')" {
if eval "contains(raw_subject_lc, '?q?')" {
# Subject header is unnecessarily encoded in quoted-printable
let "t.SUBJ_EXCESS_QP" "1";
} elsif eval "contains(raw_subject_lc, '?b?')" {
# Subject header is unnecessarily encoded in base64
let "t.SUBJ_EXCESS_BASE64" "1";
}
}
if eval "starts_with(subject_lc, 're:') && is_empty(header.in-reply-to) && is_empty(header.references)" {
# Fake reply
let "t.FAKE_REPLY" "1";
}
let "subject_lc_trim" "trim_end(subject_lc)";
if eval "subject_lc != subject_lc_trim" {
# Subject ends with space characters
let "t.SUBJECT_ENDS_SPACES" "1";
}
if eval "contains(subject_lc, '$') ||
contains(subject_lc, '€') ||
contains(subject_lc, '£') ||
contains(subject_lc, '¥')" {
# Subject contains currency symbols
let "t.SUBJECT_HAS_CURRENCY" "1";
}
if eval "ends_with(subject_lc_trim, '!')" {
# Subject ends with an exclamation mark
let "t.SUBJECT_ENDS_EXCLAIM" "1";
} elsif eval "ends_with(subject_lc_trim, '?')" {
# Subject ends with a question mark
let "t.SUBJECT_ENDS_QUESTION" "1";
}
if eval "contains(subject_lc_trim, '!')" {
# Subject contains an exclamation mark
let "t.SUBJECT_HAS_EXCLAIM" "1";
}
if eval "contains(subject_lc_trim, '?')" {
# Subject contains a question mark
let "t.SUBJECT_HAS_QUESTION" "1";
}

View File

@@ -1,125 +0,0 @@
if eval "(count(body_urls) == 1 || count(html_body_urls) == 1) && count(tokenize(text_body, 'words')) == 0" {
let "t.URL_ONLY" "1";
}
if eval "has_zwsp(urls)" {
let "t.ZERO_WIDTH_SPACE_URL" "1";
} elsif eval "has_obscured(urls)" {
let "t.R_SUSPICIOUS_URL" "1";
}
let "i" "count(urls)";
while "i > 0" {
let "i" "i - 1";
let "url" "urls[i]";
# Skip non-URLs such as 'data:' and 'mailto:'
if eval "!contains(url, '://')" {
continue;
}
let "host" "uri_part(url, 'host')";
if eval "!is_empty(host)" {
let "is_ip" "is_ip_addr(host)";
let "host" "puny_decode(host)";
let "host_lc" "to_lowercase(host)";
let "host_sld" "domain_part(host_lc, 'sld')";
# Skip local and trusted domains
if eval "is_local_domain(DOMAIN_DIRECTORY, host_sld) || key_exists('spam-allow', host_sld)" {
continue;
}
if eval "!is_ip &&
(!t.REDIRECTOR_URL || !t.URL_REDIRECTOR_NESTED) &&
key_exists('spam-redirect', host_sld)" {
let "t.REDIRECTOR_URL" "1";
let "redir_count" "1";
while "redir_count <= 5" {
# Use a custom user-agent and a 3 second timeout
let "url_redirect" "http_header(url, 'Location', 'Mozilla/5.0 (X11; Linux i686; rv:109.0) Gecko/20100101 Firefox/118.0', 3000)";
if eval "!is_empty(url_redirect)" {
let "url" "url_redirect";
let "host" "uri_part(url, 'host')";
let "is_ip" "is_ip_addr(host)";
let "host" "puny_decode(host)";
let "host_lc" "to_lowercase(host)";
let "host_sld" "domain_part(host_lc, 'sld')";
if eval "!is_ip && key_exists('spam-redirect', host_sld)" {
let "redir_count" "redir_count + 1";
} else {
break;
}
} else {
break;
}
}
if eval "redir_count > 5" {
let "t.URL_REDIRECTOR_NESTED" "1";
}
}
let "url_lc" "to_lowercase(url)";
let "query" "uri_part(url_lc, 'path_query')";
if eval "!is_ip" {
if eval "!is_ascii(host)" {
let "host_cured" "cure_text(host)";
if eval "host_lc != host_cured && dns_exists(host_cured, 'ip')" {
let "t.HOMOGRAPH_URL" "1";
}
if eval "!is_single_script(host)" {
let "t.MIXED_CHARSET_URL" "1";
}
} else {
if eval "ends_with(host, 'googleusercontent.com') && starts_with(query, '/proxy/')" {
let "t.HAS_GUC_PROXY_URI" "1";
} elsif eval "ends_with(host, 'firebasestorage.googleapis.com')" {
let "t.HAS_GOOGLE_FIREBASE_URL" "1";
} elsif eval "starts_with(domain_part(host, 'sld'), 'google.') && contains(query, 'url?') " {
let "t.HAS_GOOGLE_REDIR" "1";
}
}
if eval "(contains(host_lc, 'ipfs.') || contains(query, '/ipfs')) && contains(query, '/qm')" {
# InterPlanetary File System (IPFS) gateway URL, likely malicious
let "t.HAS_IPFS_GATEWAY_URL" "1";
} elsif eval "ends_with(host_lc, '.onion')" {
let "t.HAS_ONION_URI" "1";
}
} else {
# URL is an ip address
let "t.R_SUSPICIOUS_URL" "1";
}
if eval "starts_with(query, '/wp-')" {
# Contains WordPress URIs
let "t.HAS_WP_URI" "1";
if eval "starts_with(query, '/wp-content') | starts_with(query, '/wp-includes')" {
# URL that is pointing to a compromised WordPress installation
let "t.WP_COMPROMISED" "1";
}
}
if eval "contains(query, '/../') && !contains(query, '/well-known') && !contains(query, '/well_known')" {
# Message contains URI with a hidden path
let "t.URI_HIDDEN_PATH" "1";
}
# Phishing checks (refresh OpenPhish every 12 hours, PhishTank every 6 hours)
if eval "key_exists_http('https://openphish.com/feed.txt', url, [43200, 'list'])" {
let "t.PHISHED_OPENPHISH" "1";
}
if eval "key_exists_http('http://data.phishtank.com/data/online-valid.csv', url, [21600, 'csv', 1, ',', true])" {
let "t.PHISHED_PHISHTANK" "1";
}
} else {
# URL could not be parsed
let "t.R_SUSPICIOUS_URL" "1";
}
}