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