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