///|
pub fn parse_dmarc(text : String) -> DmarcRecord {
let raw = clean(text)
let mut valid_version = false
let mut policy = ""
let mut subdomain_policy = ""
let mut pct = 100
let mut rua = ""
let mut ruf = ""
let mut alignment_spf = "r"
let mut alignment_dkim = "r"
let mut pct_valid = true
for segment in raw.split(";") {
let item = clean(segment.to_owned())
if item.is_empty() {
continue
}
let parts = item.split("=").to_array()
if parts.length() < 2 {
continue
}
let key = lower(parts[0].trim().to_owned())
let value = parts[1].trim().to_owned()
if key == "v" && lower(value) == "dmarc1" {
valid_version = true
} else if key == "p" {
policy = lower(value)
} else if key == "sp" {
subdomain_policy = lower(value)
} else if key == "pct" {
match parse_digits(value) {
Some(number) => pct = number
None => pct_valid = false
}
} else if key == "rua" {
rua = value
} else if key == "ruf" {
ruf = value
} else if key == "aspf" {
alignment_spf = lower(value)
} else if key == "adkim" {
alignment_dkim = lower(value)
}
}
let findings = dmarc_findings(
valid_version, policy, subdomain_policy, pct, pct_valid, rua, alignment_spf,
alignment_dkim,
)
{
raw,
valid_version,
policy,
subdomain_policy,
pct,
rua_count: csv_count(rua),
ruf_count: csv_count(ruf),
alignment_spf,
alignment_dkim,
findings,
}
}
///|
fn dmarc_findings(
valid_version : Bool,
policy : String,
subdomain_policy : String,
pct : Int,
pct_valid : Bool,
rua : String,
alignment_spf : String,
alignment_dkim : String,
) -> Array[PolicyFinding] {
let findings = Array::new(capacity=10)
if valid_version {
findings.push(pass("dmarc.version", "DMARC version is present", "v=DMARC1"))
} else {
findings.push(
fail(
"dmarc.version", "DMARC version is missing", "A valid DMARC record must include v=DMARC1.",
"", "Publish a DMARC TXT record at _dmarc.example.com.",
),
)
}
if policy == "reject" {
findings.push(
pass("dmarc.policy", "DMARC rejects failing mail", "p=reject"),
)
} else if policy == "quarantine" {
findings.push(
warn(
"dmarc.policy-quarantine", "DMARC quarantines failing mail", "Quarantine is useful, but reject gives clearer spoofing protection.",
"p=quarantine", "Move to p=reject after reviewing aggregate reports.",
),
)
} else if policy == "none" {
findings.push(
warn(
"dmarc.policy-none",
"DMARC is monitor-only",
"p=none collects reports but does not ask receivers to block abuse.",
"p=none",
"Use quarantine or reject after sender alignment is confirmed.",
penalty=10,
),
)
} else if policy == "" {
findings.push(
fail(
"dmarc.policy-missing", "DMARC policy is missing", "The required p tag is absent.",
"", "Add p=none, p=quarantine or p=reject.",
),
)
} else {
findings.push(
fail(
"dmarc.policy-invalid", "DMARC policy is invalid", "The p tag must be none, quarantine or reject.",
policy, "Use a valid DMARC policy value.",
),
)
}
if subdomain_policy == "" {
findings.push(
warn(
"dmarc.subdomain-default", "DMARC has no explicit subdomain policy", "Subdomains inherit p, which may be fine but is easy to overlook.",
"", "Add sp=reject or sp=quarantine when subdomain mail is known.",
),
)
} else if subdomain_policy == "reject" || subdomain_policy == "quarantine" {
findings.push(
pass(
"dmarc.subdomain-policy",
"DMARC subdomain policy is explicit",
"sp=" + subdomain_policy,
),
)
} else if subdomain_policy == "none" {
findings.push(
warn(
"dmarc.subdomain-none", "DMARC subdomain policy is monitor-only", "sp=none leaves subdomain abuse weakly handled.",
"sp=none", "Use sp=reject when subdomains do not send mail.",
),
)
} else {
findings.push(
fail(
"dmarc.subdomain-invalid", "DMARC subdomain policy is invalid", "The sp tag must be none, quarantine or reject.",
subdomain_policy, "Use a valid sp value or remove the tag.",
),
)
}
if !pct_valid || pct < 0 || pct > 100 {
findings.push(
fail(
"dmarc.pct-invalid",
"DMARC percentage is invalid",
"pct must be an integer from 0 to 100.",
pct.to_string(),
"Use pct=100 for full enforcement.",
),
)
} else if pct < 100 {
findings.push(
warn(
"dmarc.pct-partial",
"DMARC enforcement is partial",
"Only part of the mail stream is covered by policy.",
"pct=" + pct.to_string(),
"Use pct=100 after rollout.",
),
)
} else {
findings.push(pass("dmarc.pct", "DMARC applies to all mail", "pct=100"))
}
if csv_count(rua) > 0 {
findings.push(
pass("dmarc.rua", "DMARC aggregate reporting is configured", rua),
)
} else {
findings.push(
warn(
"dmarc.rua-missing", "DMARC aggregate reporting is missing", "Without rua, operators cannot observe legitimate sender drift.",
"", "Add a rua=mailto: address for aggregate reports.",
),
)
}
if alignment_spf == "s" || alignment_spf == "r" {
findings.push(
pass("dmarc.aspf", "SPF alignment mode is valid", alignment_spf),
)
} else {
findings.push(
fail(
"dmarc.aspf-invalid", "SPF alignment mode is invalid", "aspf must be r or s.",
alignment_spf, "Use aspf=r or aspf=s.",
),
)
}
if alignment_dkim == "s" || alignment_dkim == "r" {
findings.push(
pass("dmarc.adkim", "DKIM alignment mode is valid", alignment_dkim),
)
} else {
findings.push(
fail(
"dmarc.adkim-invalid", "DKIM alignment mode is invalid", "adkim must be r or s.",
alignment_dkim, "Use adkim=r or adkim=s.",
),
)
}
findings
}