// SPF 求值(RFC 7208)。
//
// 覆盖全部机制与修饰符:all / include / a / mx / ptr / ip4 / ip6 / exists /
// redirect / exp;包含宏展开、10 次 DNS 查询上限与 2 次 void lookup 上限。
///|
pub enum SpfQualifier {
Plus
Minus
Tilde
Question
} derive(Eq)
///|
pub enum SpfKind {
All
Include
A
Mx
Ptr
Ip4
Ip6
Exists
Redirect
Exp
Unknown
} derive(Eq)
///|
pub struct SpfTerm {
qualifier : SpfQualifier
kind : SpfKind
/// 机制参数(域名、IP 或宏字符串)
value : String
cidr4 : Int?
cidr6 : Int?
raw : String
}
///|
pub struct SpfRecord {
terms : Array[SpfTerm]
}
///|
/// 宏展开上下文。
pub struct MacroCtx {
sender : String
domain : String
ip : String
helo : String
}
///|
fn spf_lower(text : StringView) -> String {
ascii_lower(text)
}
///|
fn starts_with_ignore_case(text : String, prefix : String) -> Bool {
if text.length() < prefix.length() {
return false
}
spf_lower(text[:prefix.length()]) == spf_lower(prefix)
}
///|
/// 解析 SPF 记录。返回 None 表示不是 `v=spf1` 记录。
pub fn parse_spf_record(text : String) -> SpfRecord? {
let trimmed = trim_wsp(strip_txt_quotes(text))
if !starts_with_ignore_case(trimmed, "v=spf1") {
return None
}
let terms : Array[SpfTerm] = []
let mut first = true
for raw_term in trimmed.split(" ") {
let term_text = trim_wsp(raw_term)
if term_text.length() == 0 {
continue
}
if first {
first = false
continue
}
terms.push(parse_spf_term(term_text))
}
Some({ terms, })
}
///|
fn parse_spf_term(text : String) -> SpfTerm {
// 修饰符
let eq = text.find("=")
match eq {
Some(i) => {
let name = spf_lower(text[:i])
if name == "redirect" || name == "exp" {
let kind = if name == "redirect" { Redirect } else { Exp }
return {
qualifier: Plus,
kind,
value: text[i + 1:].to_owned(),
cidr4: None,
cidr6: None,
raw: text,
}
}
}
None => ()
}
let chars = text.to_array()
let mut qualifier = Plus
let mut start = 0
if chars.length() > 0 {
let c = chars[0]
if c == '+' {
start = 1
} else if c == '-' {
qualifier = Minus
start = 1
} else if c == '~' {
qualifier = Tilde
start = 1
} else if c == '?' {
qualifier = Question
start = 1
}
}
let mut name_end = start
while name_end < chars.length() &&
chars[name_end] != ':' &&
chars[name_end] != '/' {
name_end = name_end + 1
}
let rest = StringBuilder()
for i in start.. All
"include" => Include
"a" => A
"mx" => Mx
"ptr" => Ptr
"ip4" => Ip4
"ip6" => Ip6
"exists" => Exists
_ => Unknown
}
let mut value = ""
let mut tail = ""
if name_end < chars.length() && chars[name_end] == ':' {
let mut value_end = name_end + 1
while value_end < chars.length() && chars[value_end] != '/' {
value_end = value_end + 1
}
let value_buf = StringBuilder()
let value_start = name_end + 1
for i in value_start.. (Int?, Int?) {
if tail.length() == 0 {
return (None, None)
}
// 形如 /24 或 /24//64 或 //64
let doubled = tail.find("//")
match doubled {
Some(i) => {
let first = tail[:i]
let second = tail[i + 2:]
(
parse_leading_int(first.to_owned()),
parse_leading_int(second.to_owned()),
)
}
None => (parse_leading_int(tail), None)
}
}
///|
fn parse_leading_int(text : String) -> Int? {
let chars = text.to_array()
let mut i = 0
while i < chars.length() && chars[i] == '/' {
i = i + 1
}
let start = i
while i < chars.length() && chars[i] >= '0' && chars[i] <= '9' {
i = i + 1
}
if i == start {
return None
}
let mut value = 0
for j in start.. AuthStatus {
match q {
Plus => Pass
Minus => Fail
Tilde => SoftFail
Question => Neutral
}
}
///|
fn qualifier_text(q : SpfQualifier) -> String {
match q {
Plus => "+"
Minus => "-"
Tilde => "~"
Question => "?"
}
}
///|
/// 解析 IPv4,返回四个八位组。
pub fn parse_ipv4(text : String) -> Array[Int]? {
let parts : Array[Int] = []
for part in text.split(".") {
let token = trim_wsp(part)
if token.length() == 0 || token.length() > 3 {
return None
}
let mut value = 0
for c in token {
if c < '0' || c > '9' {
return None
}
value = value * 10 + (c.to_int() - '0'.to_int())
}
if value > 255 {
return None
}
parts.push(value)
}
if parts.length() != 4 {
return None
}
Some(parts)
}
///|
/// 解析 IPv6,返回八个 16 位分组。
pub fn parse_ipv6(text : String) -> Array[Int]? {
let input = trim_wsp(text)
if input.length() == 0 {
return None
}
let groups : Array[Int] = []
let left : Array[String] = []
let right : Array[String] = []
let double_colon = input.find("::")
let mut has_double = false
let mut left_text = input
let mut right_text = ""
match double_colon {
Some(i) => {
has_double = true
left_text = input[:i].to_owned()
right_text = input[i + 2:].to_owned()
}
None => ()
}
for part in left_text.split(":") {
let token = trim_wsp(part)
if token.length() > 0 {
left.push(token)
}
}
for part in right_text.split(":") {
let token = trim_wsp(part)
if token.length() > 0 {
right.push(token)
}
}
let left_groups = parse_ipv6_side(left)
let right_groups = parse_ipv6_side(right)
let (left_values, left_ok) = left_groups
let (right_values, right_ok) = right_groups
if !left_ok || !right_ok {
return None
}
if !has_double && left_values.length() + right_values.length() != 8 {
return None
}
if has_double && left_values.length() + right_values.length() > 7 {
return None
}
for value in left_values {
groups.push(value)
}
if has_double {
let fill = 8 - left_values.length() - right_values.length()
for _ in 0.. (Array[Int], Bool) {
let values : Array[Int] = []
for part in parts {
// 末尾允许 IPv4 形式
if part.contains(".") {
match parse_ipv4(part) {
Some(v4) => {
values.push(v4[0] * 256 + v4[1])
values.push(v4[2] * 256 + v4[3])
}
None => return (values, false)
}
} else {
if part.length() > 4 {
return (values, false)
}
let mut value = 0
for c in part {
let digit = if c >= '0' && c <= '9' {
c.to_int() - '0'.to_int()
} else if c >= 'a' && c <= 'f' {
c.to_int() - 'a'.to_int() + 10
} else if c >= 'A' && c <= 'F' {
c.to_int() - 'A'.to_int() + 10
} else {
return (values, false)
}
value = value * 16 + digit
}
values.push(value)
}
}
(values, true)
}
///|
fn match_v4(ip : Array[Int], net : Array[Int], prefix : Int) -> Bool {
if prefix < 0 || prefix > 32 {
return false
}
let mut bits = 0
for i in 0..<4 {
let remain = prefix - bits
if remain <= 0 {
return true
}
if remain >= 8 {
if ip[i] != net[i] {
return false
}
bits = bits + 8
} else {
let mask = (255 << (8 - remain)) & 255
return (ip[i] & mask) == (net[i] & mask)
}
}
true
}
///|
fn match_v6(ip : Array[Int], net : Array[Int], prefix : Int) -> Bool {
if prefix < 0 || prefix > 128 {
return false
}
let mut bits = 0
for i in 0..<8 {
let remain = prefix - bits
if remain <= 0 {
return true
}
if remain >= 16 {
if ip[i] != net[i] {
return false
}
bits = bits + 16
} else {
let mask = (0xFFFF << (16 - remain)) & 0xFFFF
return (ip[i] & mask) == (net[i] & mask)
}
}
true
}
///|
fn percent_encode(text : StringView) -> String {
let out = StringBuilder()
let digits : Array[Char] = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F',
]
for c in text {
let code = c.to_int()
let safe = (code >= 48 && code <= 57) ||
(code >= 65 && code <= 90) ||
(code >= 97 && code <= 122) ||
code == 45 ||
code == 46 ||
code == 95 ||
code == 126
if safe {
out.write_char(c)
} else {
out.write_char('%')
out.write_char(digits[(code >> 4) & 15])
out.write_char(digits[code & 15])
}
}
out.to_string()
}
///|
fn find_from(text : String, needle : StringView, start : Int) -> Int? {
if start < 0 || start >= text.length() {
return None
}
let tail = text[start:].to_owned()
match tail.find(needle) {
Some(i) => Some(start + i)
None => None
}
}
///|
/// 展开 SPF 宏。`current_domain` 是正在求值的域,用于 `%{d}`。
pub fn expand_macro(
pattern : String,
ctx : MacroCtx,
current_domain : String,
) -> String {
let out = StringBuilder()
let mut i = 0
let n = pattern.length()
while i < n {
match find_from(pattern, "%{", i) {
Some(start) => {
if start > i {
out.write_string(pattern[i:start].to_owned())
}
match find_from(pattern, "}", start + 2) {
Some(end) => {
let body = pattern[start + 2:end].to_owned()
out.write_string(expand_one(body, ctx, current_domain))
i = end + 1
}
None => {
out.write_string(pattern[start:].to_owned())
i = n
}
}
}
None => {
out.write_string(pattern[i:].to_owned())
i = n
}
}
}
out.to_string()
}
///|
fn expand_one(body : String, ctx : MacroCtx, current_domain : String) -> String {
let chars = body.to_array()
if chars.length() == 0 {
return ""
}
let letter = chars[0]
let upper = letter >= 'A' && letter <= 'Z'
let lower = if upper {
Int::unsafe_to_char(letter.to_int() + 32)
} else {
letter
}
let (local, sender_domain) = match ctx.sender.split_once("@") {
Some((l, d)) => (l.to_owned(), d.to_owned())
None => ("", ctx.sender)
}
let raw = match lower {
's' => ctx.sender
'l' => local
'o' => sender_domain
'd' => current_domain
'i' => ctx.ip
'v' => if ctx.ip.contains(":") { "ip6" } else { "in-addr" }
'h' => ctx.helo
_ => ""
}
// 变换器:数字(保留右侧 N 段)、r(反转)、自定义分隔符
let mut keep = -1
let mut reverse = false
let sep_buf = StringBuilder()
for j in 1..= '0' && c <= '9' {
if keep < 0 {
keep = 0
}
keep = keep * 10 + (c.to_int() - '0'.to_int())
} else if c == 'r' || c == 'R' {
reverse = true
} else {
sep_buf.write_char(c)
}
}
let separators = sep_buf.to_string()
let seps = if separators.length() == 0 { "." } else { separators }
let mut parts : Array[String] = []
for part in raw.split(seps) {
parts.push(part.to_owned())
}
if reverse {
let reversed : Array[String] = []
let mut k = parts.length()
while k > 0 {
k = k - 1
reversed.push(parts[k])
}
parts = reversed
}
if keep > 0 && parts.length() > keep {
let trimmed : Array[String] = []
let start = parts.length() - keep
for k in start.. 0 {
joined.write_string(seps)
}
joined.write_string(parts[k])
}
let result = joined.to_string()
if upper {
percent_encode(result)
} else {
result
}
}
///|
/// DNS 查询计数与限额。
struct SpfCounter {
lookups : Array[Int]
voids : Array[Int]
}
///|
const SPF_MAX_LOOKUPS : Int = 10
///|
const SPF_MAX_VOIDS : Int = 2
///|
fn count_lookup(counter : SpfCounter) -> Bool {
counter.lookups[0] = counter.lookups[0] + 1
counter.lookups[0] <= SPF_MAX_LOOKUPS
}
///|
fn count_void(counter : SpfCounter) -> Bool {
counter.voids[0] = counter.voids[0] + 1
counter.voids[0] <= SPF_MAX_VOIDS
}
///|
/// 单域名求值结果。
struct SpfOutcome {
status : AuthStatus
mechanism : String
reason : String
}
///|
/// 对发件域做 SPF 求值。
pub fn[R : Resolver] evaluate_spf(
resolver : R,
domain : String,
ip : String,
helo? : String,
sender? : String,
) -> SpfResult {
let ctx : MacroCtx = {
sender: sender.unwrap_or("postmaster@" + domain),
domain,
ip,
helo: helo.unwrap_or(""),
}
let counter : SpfCounter = { lookups: [0], voids: [0], }
let outcome = eval_domain(resolver, normalize_name(domain), ctx, counter, 0)
{
domain: normalize_name(domain),
identity: ctx.sender,
status: outcome.status,
qualifier: "",
mechanism: outcome.mechanism,
reason: outcome.reason,
dns_lookups: counter.lookups[0],
void_lookups: counter.voids[0],
}
}
///|
fn[R : Resolver] eval_domain(
resolver : R,
domain : String,
ctx : MacroCtx,
counter : SpfCounter,
depth : Int,
) -> SpfOutcome {
if depth > 10 {
return {
status: PermError,
mechanism: "",
reason: "include/redirect recursion too deep",
}
}
let records = resolver.txt(domain)
if records.length() == 0 {
if !count_void(counter) {
return {
status: PermError,
mechanism: "",
reason: "too many void lookups",
}
}
return { status: None_, mechanism: "", reason: "no SPF record", }
}
let mut spf : SpfRecord? = None
let mut count = 0
for record in records {
match parse_spf_record(record) {
Some(parsed) => {
spf = Some(parsed)
count = count + 1
}
None => ()
}
}
if count == 0 {
return { status: None_, mechanism: "", reason: "no SPF record", }
}
if count > 1 {
return { status: PermError, mechanism: "", reason: "multiple SPF records", }
}
let record = match spf {
Some(r) => r
None => return { status: None_, mechanism: "", reason: "no SPF record", }
}
let client4 = parse_ipv4(ctx.ip)
let client6 = parse_ipv6(ctx.ip)
let mut redirect : String? = None
let mut explanation : String? = None
for term in record.terms {
if term.kind == Redirect {
redirect = Some(term.value)
continue
}
if term.kind == Exp {
explanation = Some(term.value)
continue
}
if term.kind == Unknown {
continue
}
let matched = match term.kind {
All => true
Ip4 =>
match client4 {
Some(client) =>
match parse_ipv4(term.value) {
Some(net) => match_v4(client, net, term.cidr4.unwrap_or(32))
None => false
}
None => false
}
Ip6 =>
match client6 {
Some(client) =>
match parse_ipv6(term.value) {
Some(net) => match_v6(client, net, term.cidr4.unwrap_or(128))
None => false
}
None => false
}
Include => {
if !count_lookup(counter) {
return {
status: PermError,
mechanism: term.raw,
reason: "too many DNS lookups",
}
}
let target = expand_macro(term.value, ctx, domain)
if target.length() == 0 {
false
} else {
let inner = eval_domain(resolver, target, ctx, counter, depth + 1)
inner.status == Pass
}
}
A => {
if !count_lookup(counter) {
return {
status: PermError,
mechanism: term.raw,
reason: "too many DNS lookups",
}
}
let target = if term.value.length() == 0 {
domain
} else {
expand_macro(term.value, ctx, domain)
}
match_a_or_aaaa(resolver, target, client4, client6, term, counter)
}
Mx => {
if !count_lookup(counter) {
return {
status: PermError,
mechanism: term.raw,
reason: "too many DNS lookups",
}
}
let target = if term.value.length() == 0 {
domain
} else {
expand_macro(term.value, ctx, domain)
}
let hosts = resolver.mx(target)
if hosts.length() == 0 {
if !count_void(counter) {
return {
status: PermError,
mechanism: term.raw,
reason: "too many void lookups",
}
}
false
} else {
let mut hit = false
for host in hosts {
if match_a_or_aaaa(resolver, host, client4, client6, term, counter) {
hit = true
}
}
hit
}
}
Ptr => {
if !count_lookup(counter) {
return {
status: PermError,
mechanism: term.raw,
reason: "too many DNS lookups",
}
}
let target = if term.value.length() == 0 {
domain
} else {
expand_macro(term.value, ctx, domain)
}
match_ptr(resolver, ctx.ip, target)
}
Exists => {
if !count_lookup(counter) {
return {
status: PermError,
mechanism: term.raw,
reason: "too many DNS lookups",
}
}
let target = expand_macro(term.value, ctx, domain)
let found = resolver.a(target)
if found.length() == 0 {
if !count_void(counter) {
return {
status: PermError,
mechanism: term.raw,
reason: "too many void lookups",
}
}
false
} else {
true
}
}
_ => false
}
if matched {
let mut reason = "matched \{term.raw}"
if term.qualifier == Minus {
match explanation {
Some(pattern) => {
let target = expand_macro(pattern, ctx, domain)
let texts = resolver.txt(target)
if texts.length() > 0 {
reason = reason + ": " + trim_wsp(strip_txt_quotes(texts[0]))
}
}
None => ()
}
}
return {
status: qualifier_status(term.qualifier),
mechanism: term.raw,
reason,
}
}
}
match redirect {
Some(target) => {
if !count_lookup(counter) {
return {
status: PermError,
mechanism: "",
reason: "too many DNS lookups",
}
}
let expanded = expand_macro(target, ctx, domain)
let inner = eval_domain(resolver, expanded, ctx, counter, depth + 1)
{
status: inner.status,
mechanism: "redirect=" + expanded,
reason: "redirect to \{expanded}: \{inner.reason}",
}
}
None => { status: Neutral, mechanism: "", reason: "no mechanism matched", }
}
}
///|
fn[R : Resolver] match_a_or_aaaa(
resolver : R,
host : String,
client4 : Array[Int]?,
client6 : Array[Int]?,
term : SpfTerm,
counter : SpfCounter,
) -> Bool {
match client4 {
Some(client) => {
let addresses = resolver.a(host)
if addresses.length() == 0 {
if !count_void(counter) {
return false
}
}
for address in addresses {
match parse_ipv4(address) {
Some(net) =>
if match_v4(client, net, term.cidr4.unwrap_or(32)) {
return true
}
None => ()
}
}
false
}
None =>
match client6 {
Some(client) => {
let addresses = resolver.aaaa(host)
if addresses.length() == 0 {
if !count_void(counter) {
return false
}
}
for address in addresses {
match parse_ipv6(address) {
Some(net) =>
if match_v6(client, net, term.cidr6.unwrap_or(128)) {
return true
}
None => ()
}
}
false
}
None => false
}
}
}
///|
fn[R : Resolver] match_ptr(resolver : R, ip : String, domain : String) -> Bool {
let reverse = match parse_ipv4(ip) {
Some(v4) => "\{v4[3]}.\{v4[2]}.\{v4[1]}.\{v4[0]}.in-addr.arpa"
None => return false
}
let names = resolver.ptr(reverse)
let target = normalize_name(domain)
let suffix = "." + target
for name in names {
let host = normalize_name(name)
if host == target || host.has_suffix(suffix) {
// 前向确认:主机名必须解析回同一个 IP
let addresses = resolver.a(host)
for address in addresses {
if address == ip {
return true
}
}
}
}
false
}