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