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