mta_sts/
dns.rs

1use dns_resolver::Resolver;
2use std::collections::BTreeMap;
3
4// <https://datatracker.ietf.org/doc/html/rfc8461>
5
6#[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}