use std::net::IpAddr;
use crate::contact::{Contact, EmailAddress, Scope, Source};
use crate::query::DomainName;
pub const ABUSIX_ZONE: &str = "abuse-contacts.abusix.zone";
pub const ABUSE_NET_ZONE: &str = "contacts.abuse.net";
const HEX: [u8; 16] = *b"0123456789abcdef";
pub fn abusix_name(ip: IpAddr) -> String {
let mut name = String::new();
match ip {
IpAddr::V4(v4) => {
for octet in v4.octets().iter().rev() {
name.push_str(&octet.to_string());
name.push('.');
}
}
IpAddr::V6(v6) => {
for byte in v6.octets().iter().rev() {
name.push(HEX[(byte & 0x0f) as usize] as char);
name.push('.');
name.push(HEX[(byte >> 4) as usize] as char);
name.push('.');
}
}
}
name.push_str(ABUSIX_ZONE);
name
}
pub fn abuse_net_name(domain: &DomainName) -> String {
format!("{}.{}", domain.as_str(), ABUSE_NET_ZONE)
}
pub fn rfc2142_contact(domain: &DomainName) -> Result<Contact, crate::ValidationError> {
Ok(Contact {
email: EmailAddress::new(format!("abuse@{}", domain.as_str()))?,
scope: Scope::Domain,
source: Source::Rfc2142,
})
}
pub fn names_a_mail_host<'a>(exchanges: impl IntoIterator<Item = &'a str>) -> bool {
exchanges
.into_iter()
.any(|exchange| !exchange.trim_end_matches('.').is_empty())
}
pub fn contacts_from_txt(records: &[String], scope: Scope, source: Source) -> Vec<Contact> {
records
.iter()
.flat_map(|record| record.split(','))
.filter_map(|value| EmailAddress::new(value).ok())
.map(|email| Contact {
email,
scope,
source: source.clone(),
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builds_the_abusix_name_for_v4() {
assert_eq!(
abusix_name("104.16.132.229".parse().unwrap()),
"229.132.16.104.abuse-contacts.abusix.zone"
);
}
#[test]
fn builds_the_abusix_name_for_v6() {
assert_eq!(
abusix_name("2001:4860:4860::8888".parse().unwrap()),
"8.8.8.8.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.6.8.4.0.6.8.4.1.0.0.2.\
abuse-contacts.abusix.zone"
);
}
#[test]
fn builds_the_abuse_net_name() {
let domain = "Google.com.".parse().unwrap();
assert_eq!(abuse_net_name(&domain), "google.com.contacts.abuse.net");
}
#[test]
fn builds_the_rfc2142_address() {
let domain = "example.com".parse().unwrap();
let contact = rfc2142_contact(&domain).unwrap();
assert_eq!(contact.email.as_str(), "abuse@example.com");
assert_eq!(contact.scope, Scope::Domain);
assert_eq!(contact.source, Source::Rfc2142);
}
#[test]
fn an_mx_that_names_a_host_takes_mail() {
assert!(names_a_mail_host(["mx1.example.com."]));
assert!(names_a_mail_host(["mx1.example.com"]));
}
#[test]
fn a_null_mx_takes_no_mail() {
assert!(!names_a_mail_host(["."]));
assert!(!names_a_mail_host([""]));
}
#[test]
fn a_null_mx_beside_a_real_one_does_not_hide_it() {
assert!(names_a_mail_host([".", "mx1.example.com."]));
}
#[test]
fn no_exchanges_names_no_host() {
assert!(!names_a_mail_host([]));
}
#[test]
fn splits_a_record_that_holds_more_than_one_address() {
let records = vec!["arin-contact@google.com,network-abuse@google.com".to_owned()];
let got = contacts_from_txt(&records, Scope::Network, Source::Abusix);
let emails: Vec<&str> = got.iter().map(|c| c.email.as_str()).collect();
assert_eq!(
emails,
["arin-contact@google.com", "network-abuse@google.com"]
);
}
#[test]
fn drops_a_record_that_is_not_an_address() {
let records = vec!["not-an-address".to_owned(), "abuse@example.com".to_owned()];
let got = contacts_from_txt(&records, Scope::Network, Source::Abusix);
let emails: Vec<&str> = got.iter().map(|c| c.email.as_str()).collect();
assert_eq!(emails, ["abuse@example.com"]);
}
#[test]
fn returns_nothing_when_the_zone_had_no_records() {
assert_eq!(
contacts_from_txt(&[], Scope::Network, Source::Abusix),
Vec::new()
);
}
}