1use crate::DnsError;
8use crate::{
9 Error, Version,
10 common::parse::{N, T, TagParser, TxtRecordParser, V, Y},
11};
12use mail_parser::decoders::quoted_printable::quoted_printable_decode_char;
13use std::slice::Iter;
14
15use super::{Alignment, Dmarc, Policy, Psd, Report, URI};
16
17impl TxtRecordParser for Dmarc {
18 fn parse(bytes: &[u8]) -> crate::Result<Self> {
19 let mut record = bytes.iter();
20 if record.key().unwrap_or(0) != V
21 || !record.match_bytes(b"DMARC1")
22 || !record.seek_tag_end()
23 {
24 return Err(Error::Dns(DnsError::InvalidRecordType));
25 }
26
27 let mut dmarc = Dmarc {
28 adkim: Alignment::Relaxed,
29 aspf: Alignment::Relaxed,
30 fo: Report::All,
31 np: Policy::Unspecified,
32 p: Policy::Unspecified,
33 rua: vec![],
34 ruf: vec![],
35 sp: Policy::Unspecified,
36 v: Version::V1,
37 psd: Psd::Default,
38 t: false,
39 };
40
41 while let Some(key) = record.key() {
42 match key {
43 ADKIM => {
44 dmarc.adkim = record.alignment()?;
45 }
46 ASPF => {
47 dmarc.aspf = record.alignment()?;
48 }
49 FO => {
50 dmarc.fo = record.report()?;
51 }
52 NP => {
53 dmarc.np = record.policy()?;
54 }
55 P => {
56 dmarc.p = record.policy()?;
57 }
58 RUA => {
59 dmarc.rua = record.uris()?;
60 }
61 RUF => {
62 dmarc.ruf = record.uris()?;
63 }
64 SP => {
65 dmarc.sp = record.policy()?;
66 }
67 PSD => {
68 dmarc.psd = match record.value() {
69 Y => Psd::Yes,
70 N => Psd::No,
71 _ => Psd::Default,
72 };
73 }
74 T => {
75 dmarc.t = record.value() == Y;
76 }
77 _ => {
78 record.ignore();
79 }
80 }
81 }
82
83 if dmarc.sp == Policy::Unspecified {
84 dmarc.sp = dmarc.p;
85 }
86 if dmarc.np == Policy::Unspecified {
87 dmarc.np = dmarc.sp;
88 }
89
90 Ok(dmarc)
91 }
92}
93
94pub(crate) trait DMARCParser: Sized {
95 fn alignment(&mut self) -> crate::Result<Alignment>;
96 fn report(&mut self) -> crate::Result<Report>;
97 fn policy(&mut self) -> crate::Result<Policy>;
98 fn uris(&mut self) -> crate::Result<Vec<URI>>;
99}
100
101impl DMARCParser for Iter<'_, u8> {
102 fn alignment(&mut self) -> crate::Result<Alignment> {
103 let a = match self.next_skip_whitespaces().unwrap_or(0) {
104 b'r' | b'R' => Alignment::Relaxed,
105 b's' | b'S' => Alignment::Strict,
106 _ => return Err(Error::ParseError),
107 };
108 if self.seek_tag_end() {
109 Ok(a)
110 } else {
111 Err(Error::ParseError)
112 }
113 }
114
115 fn report(&mut self) -> crate::Result<Report> {
116 let mut r = Report::All;
117
118 loop {
119 r = match self.next_skip_whitespaces().unwrap_or(0) {
120 b'0' => Report::All,
121 b'1' => Report::Any,
122 b'd' | b'D' => {
123 if r == Report::Spf {
124 Report::DkimSpf
125 } else {
126 Report::Dkim
127 }
128 }
129 b's' | b'S' => {
130 if r == Report::Dkim {
131 Report::DkimSpf
132 } else {
133 Report::Spf
134 }
135 }
136 _ => return Err(Error::ParseError),
137 };
138 match self.next_skip_whitespaces().unwrap_or(0) {
139 b':' => (),
140 b';' | 0 => return Ok(r),
141 _ => return Err(Error::ParseError),
142 }
143 }
144 }
145
146 fn policy(&mut self) -> crate::Result<Policy> {
147 let p = match self.next_skip_whitespaces().unwrap_or(0) {
148 b'n' | b'N' if self.match_bytes(b"one") => Policy::None,
149 b'q' | b'Q' if self.match_bytes(b"uarantine") => Policy::Quarantine,
150 b'r' | b'R' if self.match_bytes(b"eject") => Policy::Reject,
151 _ => return Err(Error::ParseError),
152 };
153 if self.seek_tag_end() {
154 Ok(p)
155 } else {
156 Err(Error::ParseError)
157 }
158 }
159
160 #[allow(clippy::while_let_on_iterator)]
161 fn uris(&mut self) -> crate::Result<Vec<URI>> {
162 let mut uris = Vec::new();
163 let mut uri = Vec::with_capacity(16);
164 let mut found_uri = false;
165 let mut found_at = false;
166 let mut size: usize = 0;
167
168 'outer: while let Some(&ch) = self.next() {
169 match ch {
170 b'%' => {
171 let mut hex1 = 0;
172
173 while let Some(&ch) = self.next() {
174 if ch.is_ascii_hexdigit() {
175 if hex1 != 0 {
176 if let Some(ch) = quoted_printable_decode_char(hex1, ch) {
177 match ch {
178 b'@' => {
179 found_at = true;
180 uri.push(ch);
181 }
182 _ => {
183 if !ch.is_ascii_whitespace() {
184 uri.push(ch);
185 }
186 }
187 }
188 }
189 break;
190 } else {
191 hex1 = ch;
192 }
193 } else if ch == b';' {
194 break 'outer;
195 } else if !ch.is_ascii_whitespace() {
196 return Err(Error::ParseError);
197 }
198 }
199 }
200 b'!' => {
201 let mut has_digits = false;
202 let mut has_units = false;
203
204 while let Some(&ch) = self.next() {
205 match ch {
206 b'0'..=b'9' if !has_units => {
207 size =
208 (size.saturating_mul(10)).saturating_add((ch - b'0') as usize);
209 has_digits = true;
210 }
211 b'k' | b'K' if !has_units && has_digits => {
212 size = size.saturating_mul(1024);
213 has_units = true;
214 }
215 b'm' | b'M' if !has_units && has_digits => {
216 size = size.saturating_mul(1024 * 1024);
217 has_units = true;
218 }
219 b'g' | b'G' if !has_units && has_digits => {
220 size = size.saturating_mul(1024 * 1024 * 1024);
221 has_units = true;
222 }
223 b't' | b'T' if !has_units && has_digits => {
224 size = usize::try_from(
225 (size as u64).saturating_mul(1024 * 1024 * 1024 * 1024),
226 )
227 .unwrap_or(usize::MAX);
228 has_units = true;
229 }
230 b';' => {
231 break 'outer;
232 }
233 b',' => {
234 if !uri.is_empty() {
235 if found_uri && found_at {
236 uris.push(URI {
237 uri: String::from_utf8_lossy(&uri).to_lowercase(),
238 max_size: size,
239 });
240 }
241 found_uri = false;
242 found_at = false;
243 uri.clear();
244 }
245 size = 0;
246 break;
247 }
248 _ => {
249 if !ch.is_ascii_whitespace() {
250 return Err(Error::ParseError);
251 }
252 }
253 }
254 }
255 }
256 b',' => {
257 if !uri.is_empty() {
258 if found_uri && found_at {
259 uris.push(URI {
260 uri: String::from_utf8_lossy(&uri).to_lowercase(),
261 max_size: size,
262 });
263 }
264 found_uri = false;
265 found_at = false;
266 uri.clear();
267 }
268 size = 0;
269 }
270 b':' if !found_uri => {
271 found_uri = uri.eq_ignore_ascii_case(b"mailto");
272 uri.clear();
273 }
274 b';' => {
275 break;
276 }
277 b'@' => {
278 found_at = true;
279 uri.push(ch);
280 }
281 _ => {
282 if !ch.is_ascii_whitespace() {
283 uri.push(ch);
284 }
285 }
286 }
287 }
288
289 if !uri.is_empty() && found_uri && found_at {
290 uris.push(URI {
291 uri: String::from_utf8_lossy(&uri).to_lowercase(),
292 max_size: size,
293 })
294 }
295
296 Ok(uris)
297 }
298}
299
300const ADKIM: u64 = (b'a' as u64)
301 | ((b'd' as u64) << 8)
302 | ((b'k' as u64) << 16)
303 | ((b'i' as u64) << 24)
304 | ((b'm' as u64) << 32);
305const ASPF: u64 =
306 (b'a' as u64) | ((b's' as u64) << 8) | ((b'p' as u64) << 16) | ((b'f' as u64) << 24);
307const FO: u64 = (b'f' as u64) | ((b'o' as u64) << 8);
308const NP: u64 = (b'n' as u64) | ((b'p' as u64) << 8);
309const P: u64 = b'p' as u64;
310const RUA: u64 = (b'r' as u64) | ((b'u' as u64) << 8) | ((b'a' as u64) << 16);
311const RUF: u64 = (b'r' as u64) | ((b'u' as u64) << 8) | ((b'f' as u64) << 16);
312const SP: u64 = (b's' as u64) | ((b'p' as u64) << 8);
313const PSD: u64 = (b'p' as u64) | ((b's' as u64) << 8) | ((b'd' as u64) << 16);
314
315#[cfg(test)]
316mod test {
317 use crate::{
318 Version,
319 common::parse::TxtRecordParser,
320 dmarc::{Alignment, Dmarc, Policy, Psd, Report, URI},
321 };
322
323 #[test]
324 fn parse_dmarc() {
325 for (record, expected_result) in [
326 (
327 "v=DMARC1; p=none; rua=mailto:dmarc-feedback@example.com",
328 Dmarc {
329 adkim: Alignment::Relaxed,
330 aspf: Alignment::Relaxed,
331 fo: Report::All,
332 np: Policy::None,
333 p: Policy::None,
334 rua: vec![URI::new("dmarc-feedback@example.com", 0)],
335 ruf: vec![],
336 sp: Policy::None,
337 psd: Psd::Default,
338 t: false,
339 v: Version::V1,
340 },
341 ),
342 (
343 concat!(
344 "v=DMARC1; p=none; rua=mailto:dmarc-feedback@example.com;",
345 "ruf=mailto:auth-reports@example.com"
346 ),
347 Dmarc {
348 adkim: Alignment::Relaxed,
349 aspf: Alignment::Relaxed,
350 fo: Report::All,
351 np: Policy::None,
352 p: Policy::None,
353 rua: vec![URI::new("dmarc-feedback@example.com", 0)],
354 ruf: vec![URI::new("auth-reports@example.com", 0)],
355 sp: Policy::None,
356 psd: Psd::Default,
357 t: false,
358 v: Version::V1,
359 },
360 ),
361 (
362 concat!(
363 "v=DMARC1; p=quarantine; rua=mailto:dmarc-feedback@example.com,",
364 "mailto:tld-test@thirdparty.example.net!10m; pct=25; fo=d:s"
365 ),
366 Dmarc {
367 adkim: Alignment::Relaxed,
368 aspf: Alignment::Relaxed,
369 fo: Report::DkimSpf,
370 np: Policy::Quarantine,
371 p: Policy::Quarantine,
372 ruf: vec![],
373 rua: vec![
374 URI::new("dmarc-feedback@example.com", 0),
375 URI::new("tld-test@thirdparty.example.net", 10 * 1024 * 1024),
376 ],
377 sp: Policy::Quarantine,
378 psd: Psd::Default,
379 t: false,
380 v: Version::V1,
381 },
382 ),
383 (
384 concat!(
385 "v=DMARC1; p=reject; sp=quarantine; np=None; aspf=s; adkim=s; fo = 1;",
386 "rua=mailto:dmarc-feedback@example.com"
387 ),
388 Dmarc {
389 adkim: Alignment::Strict,
390 aspf: Alignment::Strict,
391 fo: Report::Any,
392 np: Policy::None,
393 p: Policy::Reject,
394 rua: vec![URI::new("dmarc-feedback@example.com", 0)],
395 ruf: vec![],
396 sp: Policy::Quarantine,
397 psd: Psd::Default,
398 t: false,
399 v: Version::V1,
400 },
401 ),
402 (
403 concat!(
404 "v=DMARC1; p=reject; ri = 3600; aspf=r; adkim =r; ",
405 "rua=mailto:dmarc-feedback@example.com!10 K , mailto:user%20@example.com ! 2G;",
406 "ignore_me= true; fo=s; rf = AfrF; ",
407 ),
408 Dmarc {
409 adkim: Alignment::Relaxed,
410 aspf: Alignment::Relaxed,
411 fo: Report::Spf,
412 np: Policy::Reject,
413 p: Policy::Reject,
414 rua: vec![
415 URI::new("dmarc-feedback@example.com", 10 * 1024),
416 URI::new("user@example.com", 2 * 1024 * 1024 * 1024),
417 ],
418 ruf: vec![],
419 sp: Policy::Reject,
420 psd: Psd::Default,
421 t: false,
422 v: Version::V1,
423 },
424 ),
425 (
426 concat!(
427 "v=DMARC1; p=quarantine; rua=mailto:dmarc-feedback@example.com,",
428 "mailto:tld-test@thirdparty.example.net; fo=s:d; t=y; psd=y;;",
429 ),
430 Dmarc {
431 adkim: Alignment::Relaxed,
432 aspf: Alignment::Relaxed,
433 fo: Report::DkimSpf,
434 np: Policy::Quarantine,
435 p: Policy::Quarantine,
436 rua: vec![
437 URI::new("dmarc-feedback@example.com", 0),
438 URI::new("tld-test@thirdparty.example.net", 0),
439 ],
440 ruf: vec![],
441 sp: Policy::Quarantine,
442 psd: Psd::Yes,
443 t: true,
444 v: Version::V1,
445 },
446 ),
447 ] {
448 assert_eq!(
449 Dmarc::parse(record.as_bytes())
450 .unwrap_or_else(|err| panic!("{record:?} : {err:?}")),
451 expected_result,
452 "{record}"
453 );
454 }
455 }
456}