///|
pub fn parse_mtasts(text : String) -> MtastsRecord {
let raw = text.trim().to_owned()
let version = colon_value(raw, "version")
let mode = lower(colon_value(raw, "mode"))
let max_age_text = colon_value(raw, "max_age")
let id = colon_value(raw, "id")
let mx_hosts = colon_values(raw, "mx")
let mut max_age = 0
let mut max_age_valid = true
if max_age_text.is_empty() {
max_age_valid = false
} else {
match parse_digits(max_age_text) {
Some(value) => max_age = value
None => max_age_valid = false
}
}
let valid_version = lower(version) == "stsv1"
let findings = mtasts_findings(
raw, valid_version, version, mode, max_age, max_age_valid, mx_hosts,
)
{
raw,
valid_version,
version,
mode,
max_age,
id,
mx_count: mx_hosts.length(),
mx_hosts,
findings,
}
}
///|
fn mtasts_findings(
raw : String,
valid_version : Bool,
version : String,
mode : String,
max_age : Int,
max_age_valid : Bool,
mx_hosts : Array[String],
) -> Array[PolicyFinding] {
let findings = Array::new(capacity=12)
if raw.is_empty() {
findings.push(
warn(
"mtasts.policy-missing", "MTA-STS policy text is missing", "Without an MTA-STS policy file, receivers cannot enforce strict TLS delivery from this data.",
"", "Publish a policy file with version, mode, mx and max_age.",
),
)
return findings
}
if valid_version {
findings.push(
pass("mtasts.version", "MTA-STS policy version is valid", version),
)
} else {
findings.push(
fail(
"mtasts.version-invalid", "MTA-STS policy version is invalid", "The policy file must start with version: STSv1.",
version, "Set version: STSv1.",
),
)
}
if mode == "enforce" {
findings.push(
pass("mtasts.mode", "MTA-STS is enforcing TLS policy", "enforce"),
)
} else if mode == "testing" {
findings.push(
warn(
"mtasts.mode-testing", "MTA-STS is in testing mode", "Testing mode reports problems but does not request strict enforcement.",
"testing", "Move to mode: enforce after validating reports.",
),
)
} else if mode == "none" {
findings.push(
warn(
"mtasts.mode-none", "MTA-STS policy is disabled", "mode: none disables strict TLS policy.",
"none", "Use mode: testing or mode: enforce.",
),
)
} else if mode.is_empty() {
findings.push(
fail(
"mtasts.mode-missing", "MTA-STS mode is missing", "The policy needs mode: enforce, testing or none.",
"", "Add mode: testing during rollout or mode: enforce in production.",
),
)
} else {
findings.push(
fail(
"mtasts.mode-invalid", "MTA-STS mode is invalid", "Allowed modes are enforce, testing and none.",
mode, "Use a valid MTA-STS mode.",
),
)
}
if !max_age_valid {
findings.push(
fail(
"mtasts.max-age-invalid", "MTA-STS max_age is missing or invalid", "max_age must be a non-negative integer.",
"", "Add max_age: 86400 or higher.",
),
)
} else if max_age < 86400 {
findings.push(
warn(
"mtasts.max-age-low",
"MTA-STS max_age is short",
"Very short policies reduce protection and increase fetch pressure.",
max_age.to_string(),
"Use at least 86400 seconds after rollout.",
),
)
} else if max_age > 31557600 {
findings.push(
warn(
"mtasts.max-age-high",
"MTA-STS max_age is very long",
"Long cache windows make emergency rollback harder.",
max_age.to_string(),
"Keep max_age within the operational rollback window.",
penalty=3,
),
)
} else {
findings.push(
pass(
"mtasts.max-age",
"MTA-STS max_age is in a practical range",
max_age.to_string(),
),
)
}
if mx_hosts.length() == 0 {
findings.push(
fail(
"mtasts.mx-missing", "MTA-STS MX hosts are missing", "The policy must list at least one mx pattern.",
"", "Add mx: mail.example.com or another legitimate inbound MX host.",
),
)
} else {
findings.push(
pass(
"mtasts.mx",
"MTA-STS policy lists MX hosts",
mx_hosts.length().to_string(),
),
)
for host in mx_hosts {
if has_wildcard_host(host) {
findings.push(
warn(
"mtasts.mx-wildcard",
"MTA-STS MX pattern uses a wildcard",
"Wildcards are convenient but can hide accidental host coverage.",
host,
"Prefer exact MX hosts when the inbound estate is known.",
penalty=3,
),
)
} else if host.trim().is_empty() {
findings.push(
warn(
"mtasts.mx-empty",
"MTA-STS MX entry is empty",
"Empty mx entries make policy review confusing.",
"",
"Remove empty mx lines.",
penalty=2,
),
)
}
}
}
findings
}
///|
pub fn MtastsRecord::is_configured(self : MtastsRecord) -> Bool {
!self.raw.is_empty()
}
///|
pub fn MtastsRecord::is_enforcing(self : MtastsRecord) -> Bool {
self.mode == "enforce"
}
///|
pub fn MtastsRecord::is_testing(self : MtastsRecord) -> Bool {
self.mode == "testing"
}
///|
pub fn MtastsRecord::has_mx_hosts(self : MtastsRecord) -> Bool {
self.mx_count > 0
}
///|
pub fn MtastsRecord::cache_window_label(self : MtastsRecord) -> String {
if self.max_age <= 0 {
"missing"
} else if self.max_age < 86400 {
"short"
} else if self.max_age <= 604800 {
"normal"
} else if self.max_age <= 31557600 {
"long"
} else {
"very-long"
}
}