1use std::slice::Iter;
8
9use mail_parser::decoders::base64::base64_decode_stream;
10
11use crate::{
12 common::{crypto::VerifyingKeyType, parse::*, verify::DomainKey},
13 dkim::{RR_EXPIRATION, RR_SIGNATURE, RR_UNKNOWN_TAG, RR_VERIFICATION},
14 Error,
15};
16
17use super::{
18 Algorithm, Atps, Canonicalization, DomainKeyReport, Flag, HashAlgorithm, Service, Signature,
19 Version, RR_DNS, RR_OTHER, RR_POLICY,
20};
21
22const ATPSH: u64 = (b'a' as u64)
23 | ((b't' as u64) << 8)
24 | ((b'p' as u64) << 16)
25 | ((b's' as u64) << 24)
26 | ((b'h' as u64) << 32);
27const ATPS: u64 =
28 (b'a' as u64) | ((b't' as u64) << 8) | ((b'p' as u64) << 16) | ((b's' as u64) << 24);
29const NONE: u64 =
30 (b'n' as u64) | ((b'o' as u64) << 8) | ((b'n' as u64) << 16) | ((b'e' as u64) << 24);
31const SHA256: u64 = (b's' as u64)
32 | ((b'h' as u64) << 8)
33 | ((b'a' as u64) << 16)
34 | ((b'2' as u64) << 24)
35 | ((b'5' as u64) << 32)
36 | ((b'6' as u64) << 40);
37const SHA1: u64 =
38 (b's' as u64) | ((b'h' as u64) << 8) | ((b'a' as u64) << 16) | ((b'1' as u64) << 24);
39const RA: u64 = (b'r' as u64) | ((b'a' as u64) << 8);
40const RP: u64 = (b'r' as u64) | ((b'p' as u64) << 8);
41const RR: u64 = (b'r' as u64) | ((b'r' as u64) << 8);
42const RS: u64 = (b'r' as u64) | ((b's' as u64) << 8);
43const ALL: u64 = (b'a' as u64) | ((b'l' as u64) << 8) | ((b'l' as u64) << 16);
44
45impl Signature {
46 #[allow(clippy::while_let_on_iterator)]
47 pub fn parse(header: &'_ [u8]) -> crate::Result<Self> {
48 let mut signature = Signature {
49 v: 0,
50 a: Algorithm::RsaSha256,
51 d: "".into(),
52 s: "".into(),
53 i: "".into(),
54 b: Vec::with_capacity(0),
55 bh: Vec::with_capacity(0),
56 h: Vec::with_capacity(0),
57 z: Vec::with_capacity(0),
58 l: 0,
59 x: 0,
60 t: 0,
61 ch: Canonicalization::Simple,
62 cb: Canonicalization::Simple,
63 r: false,
64 atps: None,
65 atpsh: None,
66 };
67 let header_len = header.len();
68 let mut header = header.iter();
69
70 while let Some(key) = header.key() {
71 match key {
72 V => {
73 signature.v = header.number().unwrap_or(0) as u32;
74 if signature.v != 1 {
75 return Err(Error::UnsupportedVersion);
76 }
77 }
78 A => {
79 signature.a = header.algorithm()?;
80 }
81 B => {
82 signature.b =
83 base64_decode_stream(&mut header, header_len, b';').ok_or(Error::Base64)?
84 }
85 BH => {
86 signature.bh =
87 base64_decode_stream(&mut header, header_len, b';').ok_or(Error::Base64)?
88 }
89 C => {
90 let (ch, cb) = header.canonicalization(Canonicalization::Simple)?;
91 signature.ch = ch;
92 signature.cb = cb;
93 }
94 D => signature.d = header.text(true),
95 H => signature.h = header.items(),
96 I => signature.i = header.text_qp(Vec::with_capacity(20), true, false),
97 L => signature.l = header.number().unwrap_or(0),
98 S => signature.s = header.text(true),
99 T => signature.t = header.number().unwrap_or(0),
100 X => signature.x = header.number().unwrap_or(0),
101 Z => signature.z = header.headers_qp(),
102 R => signature.r = header.value() == Y,
103 ATPS => {
104 if signature.atps.is_none() {
105 signature.atps = Some(header.text(true));
106 }
107 }
108 ATPSH => {
109 signature.atpsh = match header.value() {
110 SHA256 => HashAlgorithm::Sha256.into(),
111 SHA1 => HashAlgorithm::Sha1.into(),
112 NONE => None,
113 _ => {
114 signature.atps = Some("".into());
115 None
116 }
117 };
118 }
119 _ => header.ignore(),
120 }
121 }
122
123 if !signature.d.is_empty()
124 && !signature.s.is_empty()
125 && !signature.b.is_empty()
126 && !signature.bh.is_empty()
127 && !signature.h.is_empty()
128 {
129 Ok(signature)
130 } else {
131 Err(Error::MissingParameters)
132 }
133 }
134}
135
136pub(crate) trait SignatureParser: Sized {
137 fn canonicalization(
138 &mut self,
139 default: Canonicalization,
140 ) -> crate::Result<(Canonicalization, Canonicalization)>;
141 fn algorithm(&mut self) -> crate::Result<Algorithm>;
142}
143
144impl SignatureParser for Iter<'_, u8> {
145 fn canonicalization(
146 &mut self,
147 default: Canonicalization,
148 ) -> crate::Result<(Canonicalization, Canonicalization)> {
149 let mut cb = default;
150 let mut ch = default;
151
152 let mut has_header = false;
153 let mut c = None;
154
155 while let Some(char) = self.next() {
156 match (char, c) {
157 (b's' | b'S', None) => {
158 if self.match_bytes(b"imple") {
159 c = Canonicalization::Simple.into();
160 } else {
161 return Err(Error::UnsupportedCanonicalization);
162 }
163 }
164 (b'r' | b'R', None) => {
165 if self.match_bytes(b"elaxed") {
166 c = Canonicalization::Relaxed.into();
167 } else {
168 return Err(Error::UnsupportedCanonicalization);
169 }
170 }
171 (b'/', Some(c_)) => {
172 ch = c_;
173 c = None;
174 has_header = true;
175 }
176 (b';', _) => {
177 break;
178 }
179 (_, _) => {
180 if !char.is_ascii_whitespace() {
181 return Err(Error::UnsupportedCanonicalization);
182 }
183 }
184 }
185 }
186
187 if let Some(c) = c {
188 if has_header {
189 cb = c;
190 } else {
191 ch = c;
192 }
193 }
194
195 Ok((ch, cb))
196 }
197
198 fn algorithm(&mut self) -> crate::Result<Algorithm> {
199 match self.next_skip_whitespaces().unwrap_or(0) {
200 b'r' | b'R' => {
201 if self.match_bytes(b"sa-sha") {
202 let mut algo = 0;
203
204 for ch in self {
205 match ch {
206 b'1' if algo == 0 => algo = 1,
207 b'2' if algo == 0 => algo = 2,
208 b'5' if algo == 2 => algo = 25,
209 b'6' if algo == 25 => algo = 256,
210 b';' => {
211 break;
212 }
213 _ => {
214 if !ch.is_ascii_whitespace() {
215 return Err(Error::UnsupportedAlgorithm);
216 }
217 }
218 }
219 }
220
221 match algo {
222 256 => Ok(Algorithm::RsaSha256),
223 1 => Ok(Algorithm::RsaSha1),
224 _ => Err(Error::UnsupportedAlgorithm),
225 }
226 } else {
227 Err(Error::UnsupportedAlgorithm)
228 }
229 }
230 b'e' | b'E' => {
231 if self.match_bytes(b"d25519-sha256") && self.seek_tag_end() {
232 Ok(Algorithm::Ed25519Sha256)
233 } else {
234 Err(Error::UnsupportedAlgorithm)
235 }
236 }
237 _ => Err(Error::UnsupportedAlgorithm),
238 }
239 }
240}
241
242impl TxtRecordParser for DomainKey {
243 #[allow(clippy::while_let_on_iterator)]
244 fn parse(header: &[u8]) -> crate::Result<Self> {
245 let header_len = header.len();
246 let mut header = header.iter();
247 let mut flags = 0;
248 let mut key_type = VerifyingKeyType::Rsa;
249 let mut public_key = None;
250
251 while let Some(key) = header.key() {
252 match key {
253 V => {
254 if !header.match_bytes(b"DKIM1") || !header.seek_tag_end() {
255 return Err(Error::InvalidRecordType);
256 }
257 }
258 H => flags |= header.flags::<HashAlgorithm>(),
259 P => {
260 if let Some(bytes) = base64_decode_stream(&mut header, header_len, b';') {
261 public_key = Some(bytes);
262 }
263 }
264 S => flags |= header.flags::<Service>(),
265 T => flags |= header.flags::<Flag>(),
266 K => {
267 if let Some(ch) = header.next_skip_whitespaces() {
268 match ch {
269 b'r' | b'R' => {
270 if header.match_bytes(b"sa") && header.seek_tag_end() {
271 key_type = VerifyingKeyType::Rsa;
272 } else {
273 return Err(Error::UnsupportedKeyType);
274 }
275 }
276 b'e' | b'E' => {
277 if header.match_bytes(b"d25519") && header.seek_tag_end() {
278 key_type = VerifyingKeyType::Ed25519;
279 } else {
280 return Err(Error::UnsupportedKeyType);
281 }
282 }
283 b';' => (),
284 _ => {
285 return Err(Error::UnsupportedKeyType);
286 }
287 }
288 }
289 }
290 _ => {
291 header.ignore();
292 }
293 }
294 }
295
296 match public_key {
297 Some(public_key) => Ok(DomainKey {
298 p: key_type.verifying_key(&public_key)?,
299 f: flags,
300 }),
301 _ => Err(Error::InvalidRecordType),
302 }
303 }
304}
305
306impl TxtRecordParser for DomainKeyReport {
307 #[allow(clippy::while_let_on_iterator)]
308 fn parse(header: &[u8]) -> crate::Result<Self> {
309 let mut header = header.iter();
310 let mut record = DomainKeyReport {
311 ra: String::new(),
312 rp: 100,
313 rr: u8::MAX,
314 rs: None,
315 };
316
317 while let Some(key) = header.key() {
318 match key {
319 RA => {
320 record.ra = header.text_qp(Vec::with_capacity(20), true, false);
321 }
322 RP => {
323 record.rp = std::cmp::min(header.number().unwrap_or(0), 100) as u8;
324 }
325 RS => {
326 record.rs = header.text_qp(Vec::with_capacity(20), false, false).into();
327 }
328 RR => {
329 record.rr = 0;
330 loop {
331 let (val, stop_char) = header.flag_value();
332 match val {
333 ALL => {
334 record.rr = u8::MAX;
335 }
336 D => {
337 record.rr |= RR_DNS;
338 }
339 O => {
340 record.rr |= RR_OTHER;
341 }
342 P => {
343 record.rr |= RR_POLICY;
344 }
345 S => {
346 record.rr |= RR_SIGNATURE;
347 }
348 U => {
349 record.rr |= RR_UNKNOWN_TAG;
350 }
351 V => {
352 record.rr |= RR_VERIFICATION;
353 }
354 X => {
355 record.rr |= RR_EXPIRATION;
356 }
357 _ => (),
358 }
359
360 if stop_char != b':' {
361 break;
362 }
363 }
364 }
365
366 _ => {
367 header.ignore();
368 }
369 }
370 }
371
372 if !record.ra.is_empty() {
373 Ok(record)
374 } else {
375 Err(Error::InvalidRecordType)
376 }
377 }
378}
379
380impl TxtRecordParser for Atps {
381 #[allow(clippy::while_let_on_iterator)]
382 fn parse(header: &[u8]) -> crate::Result<Self> {
383 let mut header = header.iter();
384 let mut record = Atps {
385 v: Version::V1,
386 d: None,
387 };
388 let mut has_version = false;
389
390 while let Some(key) = header.key() {
391 match key {
392 V => {
393 if !header.match_bytes(b"ATPS1") || !header.seek_tag_end() {
394 return Err(Error::InvalidRecordType);
395 }
396 has_version = true;
397 }
398 D => {
399 record.d = header.text(true).into();
400 }
401 _ => {
402 header.ignore();
403 }
404 }
405 }
406
407 if !has_version {
408 return Err(Error::InvalidRecordType);
409 }
410
411 Ok(record)
412 }
413}
414
415impl DomainKey {
416 pub fn has_flag(&self, flag: impl Into<u64>) -> bool {
417 (self.f & flag.into()) != 0
418 }
419}
420
421impl ItemParser for HashAlgorithm {
422 fn parse(bytes: &[u8]) -> Option<Self> {
423 if bytes.eq_ignore_ascii_case(b"sha256") {
424 HashAlgorithm::Sha256.into()
425 } else if bytes.eq_ignore_ascii_case(b"sha1") {
426 HashAlgorithm::Sha1.into()
427 } else {
428 None
429 }
430 }
431}
432
433impl ItemParser for Flag {
434 fn parse(bytes: &[u8]) -> Option<Self> {
435 if bytes.eq_ignore_ascii_case(b"y") {
436 Flag::Testing.into()
437 } else if bytes.eq_ignore_ascii_case(b"s") {
438 Flag::MatchDomain.into()
439 } else {
440 None
441 }
442 }
443}
444
445impl ItemParser for Service {
446 fn parse(bytes: &[u8]) -> Option<Self> {
447 if bytes.eq(b"*") {
448 Service::All.into()
449 } else if bytes.eq_ignore_ascii_case(b"email") {
450 Service::Email.into()
451 } else {
452 None
453 }
454 }
455}
456
457#[cfg(test)]
458mod test {
459 use mail_parser::decoders::base64::base64_decode;
460
461 use crate::{
462 common::{
463 crypto::{Algorithm, R_HASH_SHA1, R_HASH_SHA256},
464 parse::TxtRecordParser,
465 verify::DomainKey,
466 },
467 dkim::{
468 Canonicalization, DomainKeyReport, Signature, RR_DNS, RR_EXPIRATION, RR_OTHER,
469 RR_POLICY, RR_SIGNATURE, RR_UNKNOWN_TAG, RR_VERIFICATION, R_FLAG_MATCH_DOMAIN,
470 R_FLAG_TESTING, R_SVC_ALL, R_SVC_EMAIL,
471 },
472 };
473
474 #[test]
475 fn dkim_signature_parse() {
476 for (signature, expected_result) in [
477 (
478 concat!(
479 "v=1; a=rsa-sha256; s=default; d=stalw.art; c=relaxed/relaxed; ",
480 "bh=QoiUNYyUV+1tZ/xUPRcE+gST2zAStvJx1OK078Ylm5s=; ",
481 "b=Du0rvdzNodI6b5bhlUaZZ+gpXJi0VwjY/3qL7lS0wzKutNVCbvdJuZObGdAcv\n",
482 " eVI/RNQh2gxW4H2ynMS3B+Unse1YLJQwdjuGxsCEKBqReKlsEKT8JlO/7b2AvxR\n",
483 "\t9Q+M2aHD5kn9dbNIKnN/PKouutaXmm18QwL5EPEN9DHXSqQ=;",
484 "h=Subject:To:From; t=311923920",
485 ),
486 Signature {
487 v: 1,
488 a: Algorithm::RsaSha256,
489 d: "stalw.art".into(),
490 s: "default".into(),
491 i: "".into(),
492 bh: base64_decode(b"QoiUNYyUV+1tZ/xUPRcE+gST2zAStvJx1OK078Ylm5s=").unwrap(),
493 b: base64_decode(
494 concat!(
495 "Du0rvdzNodI6b5bhlUaZZ+gpXJi0VwjY/3qL7lS0wzKutNVCbvdJuZObGdAcv",
496 "eVI/RNQh2gxW4H2ynMS3B+Unse1YLJQwdjuGxsCEKBqReKlsEKT8JlO/7b2AvxR",
497 "9Q+M2aHD5kn9dbNIKnN/PKouutaXmm18QwL5EPEN9DHXSqQ="
498 )
499 .as_bytes(),
500 )
501 .unwrap(),
502 h: vec!["Subject".into(), "To".into(), "From".into()],
503 z: vec![],
504 l: 0,
505 x: 0,
506 t: 311923920,
507 ch: Canonicalization::Relaxed,
508 cb: Canonicalization::Relaxed,
509 r: false,
510 atps: None,
511 atpsh: None,
512 },
513 ),
514 (
515 concat!(
516 "v=1; a=rsa-sha1; d=example.net; s=brisbane;\r\n",
517 " c=simple; q=dns/txt; i=@eng.example.net;\r\n",
518 " t=1117574938; x=1118006938;\r\n",
519 " h=from:to:subject:date;\r\n",
520 " z=From:foo@eng.example.net|To:joe@example.com|\r\n",
521 " Subject:demo=20run|Date:July=205,=202005=203:44:08=20PM=20-0700;\r\n",
522 " bh=MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=;\r\n",
523 " b=dzdVyOfAKCdLXdJOc9G2q8LoXSlEniSbav+yuU4zGeeruD00lszZVoG4ZHRNiYzR",
524 ),
525 Signature {
526 v: 1,
527 a: Algorithm::RsaSha1,
528 d: "example.net".into(),
529 s: "brisbane".into(),
530 i: "@eng.example.net".into(),
531 bh: base64_decode(b"MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=").unwrap(),
532 b: base64_decode(
533 concat!(
534 "dzdVyOfAKCdLXdJOc9G2q8LoXSlEniSbav+yuU4zGe",
535 "eruD00lszZVoG4ZHRNiYzR"
536 )
537 .as_bytes(),
538 )
539 .unwrap(),
540 h: vec!["from".into(), "to".into(), "subject".into(), "date".into()],
541 z: vec![
542 "From:foo@eng.example.net".into(),
543 "To:joe@example.com".into(),
544 "Subject:demo run".into(),
545 "Date:July 5, 2005 3:44:08 PM -0700".into(),
546 ],
547 l: 0,
548 x: 1118006938,
549 t: 1117574938,
550 ch: Canonicalization::Simple,
551 cb: Canonicalization::Simple,
552 r: false,
553 atps: None,
554 atpsh: None,
555 },
556 ),
557 (
558 concat!(
559 "v=1; a = rsa - sha256; s = brisbane; d = example.com; \r\n",
560 "c = simple / relaxed; q=dns/txt; i = \r\n joe=20@\r\n",
561 " football.example.com; \r\n",
562 "h=Received : From : To :\r\n Subject : : Date : Message-ID::;;;; \r\n",
563 "bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=; \r\n",
564 "b=AuUoFEfDxTDkHlLXSZEpZj79LICEps6eda7W3deTVFOk4yAUoqOB \r\n",
565 "4nujc7YopdG5dWLSdNg6xNAZpOPr+kHxt1IrE+NahM6L/LbvaHut \r\n",
566 "KVdkLLkpVaVVQPzeRDI009SO2Il5Lu7rDNH6mZckBdrIx0orEtZV \r\n",
567 "4bmp/YzhwvcubU4=; l = 123",
568 ),
569 Signature {
570 v: 1,
571 a: Algorithm::RsaSha256,
572 d: "example.com".into(),
573 s: "brisbane".into(),
574 i: "joe @football.example.com".into(),
575 bh: base64_decode(b"2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=").unwrap(),
576 b: base64_decode(
577 concat!(
578 "AuUoFEfDxTDkHlLXSZEpZj79LICEps6eda7W3deTVFOk4yAUoqOB",
579 "4nujc7YopdG5dWLSdNg6xNAZpOPr+kHxt1IrE+NahM6L/LbvaHut",
580 "KVdkLLkpVaVVQPzeRDI009SO2Il5Lu7rDNH6mZckBdrIx0orEtZV",
581 "4bmp/YzhwvcubU4="
582 )
583 .as_bytes(),
584 )
585 .unwrap(),
586 h: vec![
587 "Received".into(),
588 "From".into(),
589 "To".into(),
590 "Subject".into(),
591 "Date".into(),
592 "Message-ID".into(),
593 ],
594 z: vec![],
595 l: 123,
596 x: 0,
597 t: 0,
598 ch: Canonicalization::Simple,
599 cb: Canonicalization::Relaxed,
600 r: false,
601 atps: None,
602 atpsh: None,
603 },
604 ),
605 ] {
606 let result = Signature::parse(signature.as_bytes()).unwrap();
607 assert_eq!(result.v, expected_result.v, "{signature:?}");
608 assert_eq!(result.a, expected_result.a, "{signature:?}");
609 assert_eq!(result.d, expected_result.d, "{signature:?}");
610 assert_eq!(result.s, expected_result.s, "{signature:?}");
611 assert_eq!(result.i, expected_result.i, "{signature:?}");
612 assert_eq!(result.b, expected_result.b, "{signature:?}");
613 assert_eq!(result.bh, expected_result.bh, "{signature:?}");
614 assert_eq!(result.h, expected_result.h, "{signature:?}");
615 assert_eq!(result.z, expected_result.z, "{signature:?}");
616 assert_eq!(result.l, expected_result.l, "{signature:?}");
617 assert_eq!(result.x, expected_result.x, "{signature:?}");
618 assert_eq!(result.t, expected_result.t, "{signature:?}");
619 assert_eq!(result.ch, expected_result.ch, "{signature:?}");
620 assert_eq!(result.cb, expected_result.cb, "{signature:?}");
621 }
622 }
623
624 #[test]
625 fn dkim_record_parse() {
626 for (record, expected_result) in [
627 (
628 concat!(
629 "v=DKIM1; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQ",
630 "KBgQDwIRP/UC3SBsEmGqZ9ZJW3/DkMoGeLnQg1fWn7/zYt",
631 "IxN2SnFCjxOCKG9v3b4jYfcTNh5ijSsq631uBItLa7od+v",
632 "/RtdC2UzJ1lWT947qR+Rcac2gbto/NMqJ0fzfVjH4OuKhi",
633 "tdY9tf6mcwGjaNBcWToIMmPSPDdQPNUYckcQ2QIDAQAB",
634 ),
635 0,
636 ),
637 (
638 concat!(
639 "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOC",
640 "AQ8AMIIBCgKCAQEAvzwKQIIWzQXv0nihasFTT3+JO23hXCg",
641 "e+ESWNxCJdVLxKL5edxrumEU3DnrPeGD6q6E/vjoXwBabpm",
642 "8F5o96MEPm7v12O5IIK7wx7gIJiQWvexwh+GJvW4aFFa0g1",
643 "3Ai75UdZjGFNKHAEGeLmkQYybK/EHW5ymRlSg3g8zydJGEc",
644 "I/melLCiBoShHjfZFJEThxLmPHNSi+KOUMypxqYHd7hzg6W",
645 "7qnq6t9puZYXMWj6tEaf6ORWgb7DOXZSTJJjAJPBWa2+Urx",
646 "XX6Ro7L7Xy1zzeYFCk8W5vmn0wMgGpjkWw0ljJWNwIpxZAj9",
647 "p5wMedWasaPS74TZ1b7tI39ncp6QIDAQAB ; t= y : s :yy:x;",
648 "s=*:email;; h= sha1:sha 256:other;; n=ignore these notes "
649 ),
650 R_HASH_SHA1
651 | R_HASH_SHA256
652 | R_SVC_ALL
653 | R_SVC_EMAIL
654 | R_FLAG_MATCH_DOMAIN
655 | R_FLAG_TESTING,
656 ),
657 (
658 concat!(
659 "p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCYtb/9Sh8nGKV7exhUFS",
660 "+cBNXlHgO1CxD9zIfQd5ztlq1LO7g38dfmFpQafh9lKgqPBTolFhZxhF1yUNT",
661 "hpV673NdAtaCVGNyx/fTYtvyyFe9DH2tmm/ijLlygDRboSkIJ4NHZjK++48hk",
662 "NP8/htqWHS+CvwWT4Qgs0NtB7Re9bQIDAQAB"
663 ),
664 0,
665 ),
666 ] {
667 assert_eq!(
668 DomainKey::parse(record.as_bytes()).unwrap().f,
669 expected_result
670 );
671 }
672 }
673
674 #[test]
675 fn dkim_report_record_parse() {
676 for (record, expected_result) in [
677 (
678 "ra=dkim-errors; rp=97; rr=v:x",
679 DomainKeyReport {
680 ra: "dkim-errors".to_string(),
681 rp: 97,
682 rr: RR_VERIFICATION | RR_EXPIRATION,
683 rs: None,
684 },
685 ),
686 (
687 "ra=postmaster; rp=1; rr=d:o:p:s:u:v:x; rs=Error=20Message;",
688 DomainKeyReport {
689 ra: "postmaster".to_string(),
690 rp: 1,
691 rr: RR_DNS
692 | RR_OTHER
693 | RR_POLICY
694 | RR_SIGNATURE
695 | RR_UNKNOWN_TAG
696 | RR_VERIFICATION
697 | RR_EXPIRATION,
698 rs: "Error Message".to_string().into(),
699 },
700 ),
701 ] {
702 assert_eq!(
703 DomainKeyReport::parse(record.as_bytes()).unwrap(),
704 expected_result
705 );
706 }
707 }
708}