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