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