1use dns_resolver::Resolver;
2use std::collections::BTreeMap;
3
4#[derive(Debug)]
7pub struct MtaStsDnsRecord {
8 pub id: String,
9 pub fields: BTreeMap<String, String>,
10}
11
12pub async fn resolve_dns_record(
13 policy_domain: &str,
14 resolver: &dyn Resolver,
15) -> anyhow::Result<MtaStsDnsRecord> {
16 let dns_name = format!("_mta-sts.{policy_domain}");
17 let res = resolver.resolve_txt(&dns_name).await?.as_txt();
18 let txt = res.join("");
19
20 let mut fields = BTreeMap::new();
21
22 for pair in txt.split(';') {
23 if pair.is_empty() {
24 continue;
25 }
26 let (key, value) = pair.split_once('=').ok_or_else(|| {
27 anyhow::anyhow!("invalid element in STS text record: {pair}. Full record: {txt}")
28 })?;
29
30 let key = key.trim();
31 let value = value.trim();
32
33 fields.insert(key.to_string(), value.to_string());
34 }
35
36 if fields.get("v").map(|s| s.as_str()) != Some("STSv1") {
37 anyhow::bail!("TXT record is not an STSv1 record {txt}");
38 }
39
40 let id = fields
41 .get("id")
42 .ok_or_else(|| anyhow::anyhow!("STSv1 TXT record is missing id parameter. {txt}"))?
43 .to_string();
44
45 Ok(MtaStsDnsRecord { id, fields })
46}
47
48#[cfg(test)]
49pub(crate) mod test {
50 use super::*;
51 use dns_resolver::TestResolver;
52
53 #[tokio::test]
54 async fn test_parse_dns_record() {
55 let resolver = TestResolver::default().with_txt(
56 "_mta-sts.gmail.com",
57 "v=STSv1; id=20190429T010101;".to_owned(),
58 );
59
60 let result = resolve_dns_record("gmail.com", &resolver).await.unwrap();
61
62 k9::snapshot!(
63 result,
64 r#"
65MtaStsDnsRecord {
66 id: "20190429T010101",
67 fields: {
68 "id": "20190429T010101",
69 "v": "STSv1",
70 },
71}
72"#
73 );
74 }
75}