mail_auth/mta_sts/
parse.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7use crate::common::parse::{TagParser, TxtRecordParser, V};
8
9use super::{MtaSts, ReportUri, TlsRpt};
10
11const ID: u64 = (b'i' as u64) | ((b'd' as u64) << 8);
12const RUA: u64 = (b'r' as u64) | ((b'u' as u64) << 8) | ((b'a' as u64) << 16);
13
14const MAILTO: u64 = (b'm' as u64)
15    | ((b'a' as u64) << 8)
16    | ((b'i' as u64) << 16)
17    | ((b'l' as u64) << 24)
18    | ((b't' as u64) << 32)
19    | ((b'o' as u64) << 40);
20const HTTPS: u64 = (b'h' as u64)
21    | ((b't' as u64) << 8)
22    | ((b't' as u64) << 16)
23    | ((b'p' as u64) << 24)
24    | ((b's' as u64) << 32);
25
26impl TxtRecordParser for MtaSts {
27    #[allow(clippy::while_let_on_iterator)]
28    fn parse(record: &[u8]) -> crate::Result<Self> {
29        let mut record = record.iter();
30        let mut id = None;
31        let mut has_version = false;
32
33        while let Some(key) = record.key() {
34            match key {
35                V => {
36                    if !record.match_bytes(b"STSv1") || !record.seek_tag_end() {
37                        return Err(crate::Error::InvalidRecordType);
38                    }
39                    has_version = true;
40                }
41                ID => {
42                    id = record.text(false).into();
43                }
44                _ => {
45                    record.ignore();
46                }
47            }
48        }
49
50        if let Some(id) = id
51            && has_version
52        {
53            return Ok(MtaSts { id });
54        }
55        Err(crate::Error::InvalidRecordType)
56    }
57}
58
59impl TxtRecordParser for TlsRpt {
60    #[allow(clippy::while_let_on_iterator)]
61    fn parse(record: &[u8]) -> crate::Result<Self> {
62        let mut record = record.iter();
63
64        if record.key().unwrap_or(0) != V
65            || !record.match_bytes(b"TLSRPTv1")
66            || !record.seek_tag_end()
67        {
68            return Err(crate::Error::InvalidRecordType);
69        }
70
71        let mut rua = Vec::new();
72
73        while let Some(key) = record.key() {
74            match key {
75                RUA => loop {
76                    match record.flag_value() {
77                        (MAILTO, b':') => {
78                            let mail_to = record.text_qp(Vec::with_capacity(20), false, true);
79                            if !mail_to.is_empty() {
80                                rua.push(ReportUri::Mail(mail_to));
81                            }
82                        }
83                        (HTTPS, b':') => {
84                            let mut url = Vec::with_capacity(20);
85                            url.extend_from_slice(b"https:");
86                            let url = record.text_qp(url, false, true);
87                            if !url.is_empty() {
88                                rua.push(ReportUri::Http(url));
89                            }
90                        }
91                        _ => {
92                            record.ignore();
93                            break;
94                        }
95                    }
96                },
97                _ => {
98                    record.ignore();
99                }
100            }
101        }
102
103        if !rua.is_empty() {
104            Ok(TlsRpt { rua })
105        } else {
106            Err(crate::Error::InvalidRecordType)
107        }
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use crate::{
114        common::parse::TxtRecordParser,
115        mta_sts::{MtaSts, ReportUri, TlsRpt},
116    };
117
118    #[test]
119    fn mta_sts_record_parse() {
120        for (mta_sts, expected_mta_sts) in [
121            (
122                "v=STSv1; id=20160831085700Z;",
123                MtaSts {
124                    id: "20160831085700Z".to_string(),
125                },
126            ),
127            (
128                "v=STSv1; id=20190429T010101",
129                MtaSts {
130                    id: "20190429T010101".to_string(),
131                },
132            ),
133        ] {
134            assert_eq!(MtaSts::parse(mta_sts.as_bytes()).unwrap(), expected_mta_sts);
135        }
136    }
137
138    #[test]
139    fn tlsrpt_parse() {
140        for (tls_rpt, expected_tls_rpt) in [
141            (
142                "v=TLSRPTv1;rua=mailto:reports@example.com",
143                TlsRpt {
144                    rua: vec![ReportUri::Mail("reports@example.com".to_string())],
145                },
146            ),
147            (
148                "v=TLSRPTv1; rua=https://reporting.example.com/v1/tlsrpt",
149                TlsRpt {
150                    rua: vec![ReportUri::Http(
151                        "https://reporting.example.com/v1/tlsrpt".to_string(),
152                    )],
153                },
154            ),
155            (
156                "v=TLSRPTv1; rua=mailto:tlsrpt@mydomain.com,https://tlsrpt.mydomain.com/v1",
157                TlsRpt {
158                    rua: vec![
159                        ReportUri::Mail("tlsrpt@mydomain.com".to_string()),
160                        ReportUri::Http("https://tlsrpt.mydomain.com/v1".to_string()),
161                    ],
162                },
163            ),
164        ] {
165            assert_eq!(TlsRpt::parse(tls_rpt.as_bytes()).unwrap(), expected_tls_rpt);
166        }
167    }
168}