Files
mailgoserver/internal/mailstore/spam.go
T

59 lines
1.8 KiB
Go
Raw Normal View History

2026-08-12 21:14:19 +01:00
package mailstore
import (
"context"
"net"
"strings"
)
// spamKeywords is a tiny, obvious-spam subject keyword list — a coarse signal only.
var spamKeywords = []string{"viagra", "casino", "lottery winner", "click here now", "wire transfer urgent", "nigerian prince"}
// SpamScore is a lightweight built-in heuristic — always runs, regardless of whether
// rspamd (rspamd.go) is also enabled; both are additive, not either/or. Higher is more
// suspicious; compare against [Mailstore] spam_reject_score.
// ponytail: naive keyword/weight heuristic, not a real Bayesian/ML scorer — upgrade or
// lean harder on rspamd if false-positive rate matters.
func SpamScore(peerIP string, headers map[string]string, dkimPass, spfPass bool) int {
score := 0
if !spfPass {
score += 2
}
if !dkimPass {
score++
}
if CheckDNSBL(peerIP) {
score += 5
}
subject := strings.ToLower(headers["subject"])
for _, kw := range spamKeywords {
if strings.Contains(subject, kw) {
score++
}
}
return score
}
// CheckDNSBL looks up peerIP against the Spamhaus ZEN DNSBL. Per RFC 5782, a listing
// response is always an A record in 127.0.0.0/8 — checking for that range (rather than
// "any resolution succeeded") avoids false positives from a resolver that hijacks
// NXDOMAIN into a search/ad page instead of returning an error.
func CheckDNSBL(peerIP string) bool {
ip := net.ParseIP(peerIP)
if ip == nil || ip.To4() == nil {
return false
}
octets := strings.Split(ip.To4().String(), ".")
reversed := octets[3] + "." + octets[2] + "." + octets[1] + "." + octets[0]
addrs, err := net.DefaultResolver.LookupHost(context.Background(), reversed+".zen.spamhaus.org")
if err != nil {
return false
}
for _, a := range addrs {
if resolved := net.ParseIP(a); resolved != nil && resolved.To4() != nil && resolved.To4()[0] == 127 {
return true
}
}
return false
}