diff --git a/crates/common/src/config/spamfilter.rs b/crates/common/src/config/spamfilter.rs index 703c9c64..8e04bb1c 100644 --- a/crates/common/src/config/spamfilter.rs +++ b/crates/common/src/config/spamfilter.rs @@ -821,6 +821,18 @@ impl Element { Element::Any => map, } } + + pub fn as_str(&self) -> &'static str { + match self { + Element::Url => "url", + Element::Domain => "domain", + Element::Email => "email", + Element::Ip => "ip", + Element::Header => "header", + Element::Body => "body", + Element::Any => "any", + } + } } pub struct IpResolver { diff --git a/crates/spam-filter/src/analysis/domain.rs b/crates/spam-filter/src/analysis/domain.rs index 903a7a3b..eae40eea 100644 --- a/crates/spam-filter/src/analysis/domain.rs +++ b/crates/spam-filter/src/analysis/domain.rs @@ -165,14 +165,12 @@ impl SpamFilterAnalyzeDomain for Server { attributes.iter().find_map(|(attr, value)| { if *attr == HREF { let value = value.as_deref()?.strip_prefix("mailto:")?; - if value.contains('@') { + let email = + Email::new(value.split_once('?').map_or(value, |(e, _)| e)); + + if email.is_valid() { return Some(ElementLocation::new( - Recipient { - email: Email::new( - value.split_once('?').map_or(value, |(e, _)| e), - ), - name: None, - }, + Recipient { email, name: None }, if is_body { Location::BodyHtml } else { @@ -203,17 +201,19 @@ impl SpamFilterAnalyzeDomain for Server { } } - emails.insert(ElementLocation::new( - Recipient { - email: email.clone(), - name: None, - }, - if is_body { - Location::BodyText - } else { - Location::Attachment - }, - )); + if email.is_valid() { + emails.insert(ElementLocation::new( + Recipient { + email: email.clone(), + name: None, + }, + if is_body { + Location::BodyText + } else { + Location::Attachment + }, + )); + } } } } diff --git a/crates/spam-filter/src/analysis/url.rs b/crates/spam-filter/src/analysis/url.rs index 158bc6a9..c3d5f828 100644 --- a/crates/spam-filter/src/analysis/url.rs +++ b/crates/spam-filter/src/analysis/url.rs @@ -250,14 +250,16 @@ impl SpamFilterAnalyzeUrl for Server { } // Check Domain DNSBL - check_dnsbl( - self, - ctx, - &StringResolver(host.sld_or_default()), - Element::Domain, - el.location, - ) - .await; + if let Some(sld) = &host.sld { + check_dnsbl( + self, + ctx, + &StringResolver(sld), + Element::Domain, + el.location, + ) + .await; + } } else { // URL is an ip address ctx.result.add_tag("SUSPICIOUS_URL"); diff --git a/crates/spam-filter/src/modules/dnsbl.rs b/crates/spam-filter/src/modules/dnsbl.rs index 653319e3..4412a284 100644 --- a/crates/spam-filter/src/modules/dnsbl.rs +++ b/crates/spam-filter/src/modules/dnsbl.rs @@ -55,6 +55,7 @@ pub(crate) async fn check_dnsbl( server, dnsbl, SpamFilterResolver::new(ctx, resolver, location), + scope, &mut checks, ) .await @@ -77,6 +78,7 @@ async fn is_dnsbl( server: &Server, config: &DnsBlServer, resolver: SpamFilterResolver<'_, impl ResolveVariable>, + element: Element, checks: &mut usize, ) -> Option { let time = Instant::now(); @@ -133,6 +135,7 @@ async fn is_dnsbl( .iter() .map(|ip| trc::Value::from(ip.to_string())) .collect::>(), + Details = element.as_str(), Elapsed = time.elapsed() ); @@ -159,6 +162,7 @@ async fn is_dnsbl( Spam(SpamEvent::Dnsbl), Hostname = zone.clone(), Result = trc::Value::None, + Details = element.as_str(), Elapsed = time.elapsed() ); @@ -175,6 +179,7 @@ async fn is_dnsbl( Spam(SpamEvent::DnsblError), Hostname = zone, Elapsed = time.elapsed(), + Details = element.as_str(), CausedBy = err.to_string() ); diff --git a/crates/spam-filter/src/modules/sanitize.rs b/crates/spam-filter/src/modules/sanitize.rs index 4cf53e40..2a8d87d4 100644 --- a/crates/spam-filter/src/modules/sanitize.rs +++ b/crates/spam-filter/src/modules/sanitize.rs @@ -10,7 +10,7 @@ use crate::{Email, Hostname}; impl Hostname { pub fn new(host: &str) -> Self { - let mut fqdn = host.to_lowercase(); + let mut fqdn = host.trim_end_matches('.').to_lowercase(); // Decode punycode if fqdn.contains("xn--") { @@ -42,12 +42,20 @@ impl Hostname { .ok(); Hostname { - ip, sld: if ip.is_none() { - psl::domain_str(&fqdn).map(str::to_string) + psl::domain(fqdn.as_bytes()).and_then(|domain| { + if domain.suffix().typ().is_some() { + std::str::from_utf8(domain.as_bytes()) + .ok() + .map(str::to_string) + } else { + None + } + }) } else { None }, + ip, fqdn, } } diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index 7655c57f..acec2f04 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -380,7 +380,10 @@ pub fn sanitize_email(email: &str) -> Option { } } - if found_domain && last_ch != '.' && psl::domain(result.as_bytes()).is_some() { + if found_domain + && last_ch != '.' + && psl::domain(result.as_bytes()).is_some_and(|d| d.suffix().typ().is_some()) + { Some(result) } else { None diff --git a/tests/src/smtp/inbound/antispam.rs b/tests/src/smtp/inbound/antispam.rs index f0ce9276..07de86ae 100644 --- a/tests/src/smtp/inbound/antispam.rs +++ b/tests/src/smtp/inbound/antispam.rs @@ -102,6 +102,54 @@ path = "{PATH}/test_antispam.db" #type = "redis" #url = "redis://127.0.0.1" +[http-lookup.STWT_OPENPHISH] +enable = true +url = "https://openphish.com/feed.txt" +format = "list" +retry = "1h" +refresh = "12h" +timeout = "30s" +limits.size = 104857600 +limits.entries = 900000 +limits.entry-size = 512 + +[http-lookup.STWT_PHISHTANK] +enable = true +url = "http://data.phishtank.com/data/online-valid.csv.gz" +format = "csv" +separator = "," +index.key = 1 +skip-first = true +gzipped = true +retry = "1h" +refresh = "6h" +timeout = "30s" +limits.size = 104857600 +limits.entries = 900000 +limits.entry-size = 512 + +[http-lookup.STWT_DISPOSABLE_DOMAINS] +enable = true +url = "https://disposable.github.io/disposable-email-domains/domains_mx.txt" +format = "list" +retry = "1h" +refresh = "24h" +timeout = "30s" +limits.size = 104857600 +limits.entries = 900000 +limits.entry-size = 512 + +[http-lookup.STWT_FREE_DOMAINS] +enable = true +url = "https://gist.githubusercontent.com/okutbay/5b4974b70673dfdcc21c517632c1f984/raw/993a35930a8d24a1faab1b988d19d38d92afbba4/free_email_provider_domains.txt" +format = "list" +retry = "1h" +refresh = "720h" +timeout = "30s" +limits.size = 104857600 +limits.entries = 900000 +limits.entry-size = 512 + [enterprise.ai.dummy] url = "https://127.0.0.1:9090/v1/chat/completions" type = "chat"