This commit is contained in:
mdecimus
2023-10-17 20:56:48 +02:00
parent d448ac6353
commit 30092ce92c
29 changed files with 1215 additions and 95 deletions

View File

@@ -1,5 +1,12 @@
if eval "!t.SPAM_TRAP && !t.TRUSTED_REPLY" {
let "bayes_result" "bayes_classify('spamdb/token-lookup', body_and_subject)";
# Classification parameters
# min_token_hits: 2
# min_tokens: 11
# min_prob_strength: 0.05
# min_learns: 200
let "bayes_result" "bayes_classify('spamdb/token-lookup', body_and_subject, [2, 11, 0.05, 200])";
if eval "!is_empty(bayes_result)" {
if eval "bayes_result > 0.7" {
let "t.BAYES_SPAM" "1";

View File

@@ -0,0 +1,33 @@
# Whether to add an X-Spam-Status header
let "ADD_HEADER_SPAM" "true";
# Whether to add an X-Spam-Result header
let "ADD_HEADER_SPAM_RESULT" "true";
# Whether message replies from authenticated users should be learned as ham
let "AUTOLEARN_REPLIES_HAM" "true";
# Whether the bayes classifier should be trained automatically
let "AUTOLEARN_ENABLE" "true";
# When to learn ham (score >= threshold)
let "AUTOLEARN_HAM_THRESHOLD" "-0.5";
# When to learn spam (score <= threshold)
let "AUTOLEARN_SPAM_THRESHOLD" "6.0";
# Keep difference for spam/ham learns for at least this value
let "AUTOLEARN_SPAM_HAM_BALANCE" "0.9";
# If ADD_HEADER_SPAM is enabled, mark as SPAM messages with a score above this threshold
let "SCORE_SPAM_THRESHOLD" "5.0";
# Discard messages with a score above this threshold
let "SCORE_DISCARD_THRESHOLD" "0";
# Reject messages with a score above this threshold
let "SCORE_REJECT_THRESHOLD" "0";
# Directory name to use for local domain lookups
let "DOMAIN_DIRECTORY" "'default'";

View File

@@ -0,0 +1,58 @@
# Train the bayes classifier automatically
if eval "AUTOLEARN_ENABLE && (score >= AUTOLEARN_SPAM_THRESHOLD || score <= AUTOLEARN_HAM_THRESHOLD)" {
let "is_spam" "score >= AUTOLEARN_SPAM_THRESHOLD";
eval "bayes_is_balanced('spamdb/token-lookup', is_spam, AUTOLEARN_SPAM_HAM_BALANCE) &&
bayes_train('spamdb/token-insert', body_and_subject, is_spam)";
}
add balance
# Iterate over tags
let "tags" "var_names()";
let "i" "count(tags)";
let "spam_result" "";
while "i > 0" {
let "i" "i - 1";
let "tag" "tags[i]";
let "tag_score" "map('spam/scores', tag)";
if eval "is_number(tag_score)" {
let "score" "score + tag_score";
if eval "ADD_HEADER_SPAM_RESULT" {
if eval "!is_empty(spam_result)" {
let "spam_result" "spam_result + ',\r\n\t' + tag + ' (' + tag_score + ')'";
} else {
let "spam_result" "spam_result + tag + ' (' + tag_score + ')'";
}
}
} elsif eval "tag_score == 'reject' {
let "SCORE_REJECT_THRESHOLD" "1";
let "score" "2";
break;
} else if eval "tag_score == 'discard'" {
discard;
stop;
}
}
# Process score actions
if "SCORE_REJECT_THRESHOLD && score >= SCORE_REJECT_THRESHOLD" {
reject "Your message has been rejected because it has an excessive spam score. If you feel this is an error, please contact the postmaster.";
stop;
} else if "SCORE_DISCARD_THRESHOLD && score >= SCORE_DISCARD_THRESHOLD" {
discard;
stop;
} else if "ADD_HEADER_SPAM" {
let "spam_status" "";
if eval "score >= SCORE_SPAM_THRESHOLD" {
let "spam_status" "'Yes, score=' + score";
} else {
let "spam_status" "'No, score=' + score";
}
eval "add-header('X-Spam-Status', spam_status)";
if eval "!is_empty(spam_result)" {
eval "add-header('X-Spam-Result', spam_result)";
}
}

View File

@@ -0,0 +1,9 @@
set "triplet" "${env.remote_ip}.${envelope.from}.${envelope.to}";
if eval "!lookup('spamdb/id-lookup', triplet)" {
# Greylist sender for 30 days
eval "lookup_map('spamdb/id-insert', [triplet, 2592000])";
reject "422 4.2.2 Greylisted, please try again in a few moments.";
stop;
}

View File

@@ -9,6 +9,11 @@ if eval "!header.mime-version.exists" {
let "has_text_part" "0";
let "is_encrypted" "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
let "t.MIME_HEADER_CTYPE_ONLY" "1";
}
foreverypart {
let "content_type" "to_lowercase(header.content-type)";
let "type" "to_lowercase(header.content-type.type)";
@@ -138,13 +143,8 @@ foreverypart {
}
}
if eval "is_empty(type)" {
if eval "header.content-type.exists" {
let "t.BROKEN_CONTENT_TYPE" "1";
}
} elsif eval "!header.Content-Disposition:Content-Transfer-Encoding:MIME-Version.exists && (type != 'text' || subtype != 'plain')" {
# Only Content-Type header without other MIME headers
let "t.MIME_HEADER_CTYPE_ONLY" "1";
if eval "is_empty(type) && header.content-type.exists" {
let "t.BROKEN_CONTENT_TYPE" "1";
}
if eval "part_is_attachment" {

View File

@@ -38,3 +38,6 @@ 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";

View File

@@ -171,7 +171,8 @@ while "i < domains_len" {
let "domain" "domains[i]";
let "i" "i + 1";
if eval "!contains(domain, '.') || is_ip_addr(domain)" {
# Skip invalid and local domain names
if eval "!contains(domain, '.') || is_ip_addr(domain) || is_local_domain(DOMAIN_DIRECTORY, domain_part(domain, 'sld'))" {
continue;
}
@@ -286,7 +287,8 @@ while "i < emails_len" {
let "email" "emails[i]";
let "i" "i + 1";
if eval "!contains(email, '@')" {
# Skip invalid and local e-mail addresses
if eval "!contains(email, '@') || is_local_domain(DOMAIN_DIRECTORY, domain_part(email_part(email, 'domain'), 'sld'))" {
continue;
}
@@ -319,6 +321,11 @@ while "i < urls_len" {
break;
}
# Skip URLs pointing to local domains
if eval "is_local_domain(DOMAIN_DIRECTORY, domain_part(uri_part(url, 'host'), 'sld'))" {
continue;
}
# Query SURBL HASHBL
let "result" "rsplit_once(dns_query(hash(url, 'md5') + '.hashbl.surbl.org', 'ipv4')[0], '.')";
if eval "starts_with(result[0], '127.0.')" {

View File

@@ -3,9 +3,10 @@
let "message_id" "header.Message-ID";
if eval "!is_empty(message_id)" {
eval "lookup('spamdb/id-insert', message_id)";
# Store the message ID for 30 days
eval "lookup_map('spamdb/id-insert', [message_id, 2592000])";
if eval "lookup('spam/options', 'AUTOLEARN_REPLIES')" {
if eval "AUTOLEARN_ENABLE && AUTOLEARN_REPLIES_HAM && bayes_is_balanced('spamdb/token-lookup', false, AUTOLEARN_SPAM_HAM_BALANCE)" {
eval "bayes_train('spamdb/token-insert', thread_name(header.subject) + ' ' + body.to_text, false)";
}
}

View File

View File

@@ -1,6 +1,9 @@
# Check if the message was sent to a spam trap address
if eval "lookup('spam/trap-address', envelope.to)" {
eval "bayes_train('spamdb/token-insert', body_and_subject, true)";
if eval "AUTOLEARN_ENABLE && lookup('spam/trap-address', envelope.to)" {
eval "bayes_is_balanced('spamdb/token-lookup', false, AUTOLEARN_SPAM_HAM_BALANCE) && bayes_train('spamdb/token-insert', body_and_subject, true)";
let "t.SPAM_TRAP" "1";
# Disable autolearn so the classifier is not trained twice
let "AUTOLEARN_ENABLE" "0";
}

View File

@@ -12,6 +12,12 @@ 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)" {
@@ -20,6 +26,10 @@ while "i > 0" {
let "host_lc" "to_lowercase(host)";
let "host_sld" "domain_part(host_lc, 'sld')";
if eval "is_local_domain(DOMAIN_DIRECTORY, host_sld)" {
continue;
}
if eval "!is_ip &&
(!t.REDIRECTOR_URL || !t.URL_REDIRECTOR_NESTED) &&
lookup('spam/redirectors', host_sld)" {