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