///|
pub fn parse_spf(text : String) -> SpfRecord {
let raw = clean(text)
let tokens = raw.split(" ").to_array()
let mut valid_version = false
let mechanisms = Array::new(capacity=tokens.length())
let mut redirect = ""
let mut explanation = ""
let mut dns_lookup_count = 0
let mut all_qualifier = ""
for i in 0.. SpfMechanism {
let qualifier = spf_qualifier(token)
let body = strip_spf_qualifier(token)
let colon_parts = body.split(":").to_array()
let left = if colon_parts.length() > 0 {
colon_parts[0].to_owned()
} else {
body
}
let slash_parts = left.split("/").to_array()
let name = lower(
if slash_parts.length() > 0 {
slash_parts[0].to_owned()
} else {
left
},
)
let value = if colon_parts.length() >= 2 {
colon_parts[1].split("/").to_array()[0].to_owned()
} else {
""
}
{
raw: token,
qualifier,
name,
value,
dns_lookup: spf_dns_lookup(name),
terminal: name == "all",
}
}
///|
fn spf_qualifier(token : String) -> String {
if token.has_prefix("+") ||
token.has_prefix("-") ||
token.has_prefix("~") ||
token.has_prefix("?") {
token[0:1].to_owned()
} else {
"+"
}
}
///|
fn strip_spf_qualifier(token : String) -> String {
if token.has_prefix("+") ||
token.has_prefix("-") ||
token.has_prefix("~") ||
token.has_prefix("?") {
token[1:token.length()].to_owned()
} else {
token
}
}
///|
fn spf_dns_lookup(name : String) -> Bool {
name == "include" ||
name == "a" ||
name == "mx" ||
name == "ptr" ||
name == "exists"
}
///|
fn spf_findings(
valid_version : Bool,
mechanisms : Array[SpfMechanism],
redirect : String,
dns_lookup_count : Int,
all_qualifier : String,
) -> Array[PolicyFinding] {
let findings = Array::new(capacity=8)
if valid_version {
findings.push(pass("spf.version", "SPF version is present", "v=spf1"))
} else {
findings.push(
fail(
"spf.version", "SPF version is missing", "A valid SPF record must start with v=spf1.",
"", "Publish a TXT record that starts with v=spf1.",
),
)
}
if dns_lookup_count > 10 {
findings.push(
fail(
"spf.lookup-limit",
"SPF DNS lookup limit is exceeded",
"SPF evaluation should stay within 10 DNS-query mechanisms.",
dns_lookup_count.to_string(),
"Remove unnecessary include, a, mx, ptr, exists or redirect mechanisms.",
),
)
} else if dns_lookup_count >= 8 {
findings.push(
warn(
"spf.lookup-headroom",
"SPF DNS lookup headroom is low",
"The record is close to the 10-lookup limit.",
dns_lookup_count.to_string(),
"Keep at least a little lookup budget for future mail providers.",
),
)
} else {
findings.push(
pass(
"spf.lookup-limit",
"SPF DNS lookup count is within limit",
dns_lookup_count.to_string(),
),
)
}
if all_qualifier == "-" {
findings.push(pass("spf.all", "SPF ends with hard fail", "-all"))
} else if all_qualifier == "~" {
findings.push(
warn(
"spf.softfail", "SPF uses soft fail", "Soft fail allows receivers to treat unauthorized senders leniently.",
"~all", "Use -all after confirming all legitimate senders are listed.",
),
)
} else if all_qualifier == "+" {
findings.push(
fail(
"spf.open-all", "SPF allows every sender", "+all permits any host to send mail for the domain.",
"+all", "Replace +all with -all or a carefully scoped policy.",
),
)
} else if all_qualifier == "?" {
findings.push(
warn(
"spf.neutral-all", "SPF ends with neutral policy", "?all gives weak receiver guidance.",
"?all", "Use -all or ~all according to rollout readiness.",
),
)
} else if !redirect.is_empty() {
findings.push(
pass("spf.redirect", "SPF delegates policy with redirect", redirect),
)
} else {
findings.push(
warn(
"spf.no-all", "SPF has no terminal all mechanism", "Without all or redirect, unmatched senders are harder to classify.",
"", "Add -all, ~all or redirect after all legitimate senders are listed.",
),
)
}
for mechanism in mechanisms {
if mechanism.name == "ptr" {
findings.push(
warn(
"spf.ptr",
"SPF uses ptr mechanism",
"ptr is slow and fragile for receiver-side policy evaluation.",
mechanism.raw,
"Replace ptr with include, ip4, ip6, a or mx mechanisms.",
),
)
}
}
findings
}