// DMARC 对齐与策略(RFC 7489)。
//
// 对齐分 strict / relaxed 两种;relaxed 需要「组织域」,这里实现可替换的
// 公共后缀表:内置常见多级后缀,调用方也可以只依赖精确比较。

///|
pub enum DmarcAlign {
  AlignRelaxed
  AlignStrict
}

///|
pub struct DmarcPolicy {
  policy : String
  subdomain_policy : String?
  pct : Int
  adkim : DmarcAlign
  aspf : DmarcAlign
  rua : Array[String]
  ruf : Array[String]
}

///|
/// 常见多级公共后缀。完整 PSL 很大,这里覆盖中文互联网最常见的组合,
/// 其余域名按「最后两级」计算组织域。
let multi_part_suffixes : Array[String] = [
  "co.uk", "org.uk", "ac.uk", "gov.uk", "com.cn", "net.cn", "org.cn", "gov.cn", "edu.cn",
  "com.au", "net.au", "org.au", "co.jp", "ne.jp", "or.jp", "co.kr", "or.kr", "com.hk",
  "com.tw", "org.tw", "co.in", "com.br", "com.mx", "co.za",
]

///|
/// 计算组织域:`a.b.example.co.uk` → `example.co.uk`。
pub fn organizational_domain(domain : String) -> String {
  let normalized = normalize_name(domain)
  let labels : Array[String] = []
  for part in normalized.split(".") {
    labels.push(part.to_owned())
  }
  if labels.length() <= 2 {
    return normalized
  }
  let last_two = labels[labels.length() - 2] + "." + labels[labels.length() - 1]
  let mut is_multi = false
  for suffix in multi_part_suffixes {
    if suffix == last_two {
      is_multi = true
    }
  }
  if is_multi && labels.length() >= 3 {
    return labels[labels.length() - 3] + "." + last_two
  }
  last_two
}

///|
fn parse_align(value : String?) -> DmarcAlign {
  match value {
    Some(text) => if trim_wsp(text) == "s" { AlignStrict } else { AlignRelaxed }
    None => AlignRelaxed
  }
}

///|
/// 解析 DMARC 记录。返回 None 表示不是 `v=DMARC1`。
pub fn parse_dmarc_record(text : String) -> DmarcPolicy? {
  let trimmed = trim_wsp(strip_txt_quotes(text))
  if !starts_with_ignore_case(trimmed, "v=DMARC1") {
    return None
  }
  let tags = parse_tags(trimmed)
  let policy = tag_get(tags, "p").unwrap_or("none")
  let subdomain_policy = tag_get(tags, "sp")
  let pct = match tag_get(tags, "pct") {
    Some(value) =>
      match parse_int_tag(Some(value)) {
        Some(number) =>
          if number < 0 {
            0
          } else if number > 100 {
            100
          } else {
            number
          }
        None => 100
      }
    None => 100
  }
  let sub = match subdomain_policy {
    Some(value) => Some(ascii_lower(trim_wsp(value)))
    None => None
  }
  Some({
    policy: ascii_lower(trim_wsp(policy)),
    subdomain_policy: sub,
    pct,
    adkim: parse_align(tag_get(tags, "adkim")),
    aspf: parse_align(tag_get(tags, "aspf")),
    rua: split_uri_list(tag_get(tags, "rua")),
    ruf: split_uri_list(tag_get(tags, "ruf")),
  })
}

///|
fn split_uri_list(value : String?) -> Array[String] {
  let out : Array[String] = []
  match value {
    None => ()
    Some(text) =>
      for part in text.split(",") {
        let uri = trim_wsp(part)
        if uri.length() > 0 {
          out.push(uri)
        }
      }
  }
  out
}

///|
fn domains_aligned(a : String, b : String, mode : DmarcAlign) -> Bool {
  let left = normalize_name(a)
  let right = normalize_name(b)
  match mode {
    AlignStrict => left == right
    AlignRelaxed => organizational_domain(left) == organizational_domain(right)
  }
}

///|
/// DMARC 求值。`sample` 用于 `pct` 采样:0 <= sample < pct 时才执行策略。
pub fn[R : Resolver] evaluate_dmarc(
  resolver : R,
  from_domain : String,
  dkim : Array[DkimResult],
  spf : SpfResult?,
  sample? : Int,
) -> DmarcResult {
  let domain = normalize_name(from_domain)
  let mut records = resolver.txt("_dmarc." + domain)
  // RFC 7489 6.6.3:子域没有记录时回退到组织域
  if records.length() == 0 {
    let org = organizational_domain(domain)
    if org != domain {
      records = resolver.txt("_dmarc." + org)
    }
  }
  let mut policy : DmarcPolicy? = None
  for record in records {
    match parse_dmarc_record(record) {
      Some(parsed) => policy = Some(parsed)
      None => ()
    }
  }
  let policy = match policy {
    Some(p) => p
    None =>
      return {
        domain,
        policy: "",
        disposition: "none",
        dkim_aligned: false,
        spf_aligned: false,
        status: None_,
        reason: "no DMARC record at _dmarc.\{domain}",
      }
  }
  let mut dkim_aligned = false
  for result in dkim {
    if result.status == Pass &&
      domains_aligned(result.domain, domain, policy.adkim) {
      dkim_aligned = true
    }
  }
  let mut spf_aligned = false
  match spf {
    Some(result) =>
      if result.status == Pass &&
        domains_aligned(result.domain, domain, policy.aspf) {
        spf_aligned = true
      }
    None => ()
  }
  if dkim_aligned || spf_aligned {
    let dkim_state = if dkim_aligned { "pass" } else { "fail" }
    let spf_state = if spf_aligned { "pass" } else { "fail" }
    return {
      domain,
      policy: policy.policy,
      disposition: "none",
      dkim_aligned,
      spf_aligned,
      status: Pass,
      reason: "aligned identifier passed (dkim=" +
      dkim_state +
      ", spf=" +
      spf_state +
      ")",
    }
  }
  // 子域策略优先
  let effective = if domain != organizational_domain(domain) {
    match policy.subdomain_policy {
      Some(sp) => sp
      None => policy.policy
    }
  } else {
    policy.policy
  }
  let within_sample = match sample {
    Some(value) => value < policy.pct
    None => policy.pct >= 100
  }
  let disposition = if within_sample { effective } else { "none" }
  {
    domain,
    policy: policy.policy,
    disposition,
    dkim_aligned,
    spf_aligned,
    status: Fail,
    reason: "no aligned identifier passed, policy=\{effective}, pct=\{policy.pct}, disposition=\{disposition}",
  }
}

///|
/// 生成 DMARC 聚合报告(最小可用 XML)。
pub fn dmarc_aggregate_report(
  report : AuthReport,
  org_name : String,
  email : String,
  begin : Int,
  end : Int,
  domain : String,
) -> String {
  let mut dkim_pass = 0
  let mut dkim_fail = 0
  for result in report.dkim {
    if result.status == Pass {
      dkim_pass = dkim_pass + 1
    } else {
      dkim_fail = dkim_fail + 1
    }
  }
  let mut spf_pass = 0
  let mut spf_fail = 0
  match report.spf {
    Some(result) =>
      if result.status == Pass {
        spf_pass = 1
      } else {
        spf_fail = 1
      }
    None => ()
  }
  let disposition = match report.dmarc {
    Some(result) => result.disposition
    None => "none"
  }
  let policy = match report.dmarc {
    Some(result) => result.policy
    None => "none"
  }
  let xml = StringBuilder()
  xml.write_string("\n")
  xml.write_string("\n")
  xml.write_string("  \n")
  xml.write_string("    \{xml_escape(org_name)}\n")
  xml.write_string("    \{xml_escape(email)}\n")
  xml.write_string(
    "    \{begin}\{end}\n",
  )
  xml.write_string("  \n")
  xml.write_string("  \n")
  xml.write_string("    \{xml_escape(domain)}\n")
  xml.write_string("    

\{xml_escape(policy)}

\n") xml.write_string("
\n") xml.write_string(" \n") xml.write_string(" \n") xml.write_string( " \{dkim_pass}\{dkim_fail}\n", ) xml.write_string( " \{spf_pass}\{spf_fail}\n", ) xml.write_string(" \n") xml.write_string( " \{xml_escape(disposition)}\n", ) xml.write_string(" \n") xml.write_string("
\n") xml.to_string() } ///| /// XML 最小转义。 pub fn xml_escape(text : StringView) -> String { let out = StringBuilder() for c in text { match c { '<' => out.write_string("<") '>' => out.write_string(">") '&' => out.write_string("&") '"' => out.write_string(""") _ => out.write_char(c) } } out.to_string() }