1use super::{Macro, Mechanism, Qualifier, Spf, Variables};
8use crate::DnsError;
9use crate::Instant;
10use crate::{
11 Error, MX, MessageAuthenticator, Parameters, RecordSet, ResolverCache, SpfOutput, SpfResult,
12 Txt, common::cache::NoCache,
13};
14use std::borrow::Cow;
15use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
16
17pub struct SpfParameters<'x> {
18 ip: IpAddr,
19 domain: &'x str,
20 helo_domain: &'x str,
21 host_domain: &'x str,
22 sender: Sender<'x>,
23}
24
25enum Sender<'x> {
26 Ehlo(String),
27 MailFrom(&'x str),
28 Full(&'x str),
29}
30
31#[allow(clippy::iter_skip_zero)]
32impl MessageAuthenticator {
33 pub async fn verify_spf<'x, TXT, MXX, IPV4, IPV6, PTR>(
35 &self,
36 params: impl Into<Parameters<'x, SpfParameters<'x>, TXT, MXX, IPV4, IPV6, PTR>>,
37 ) -> SpfOutput
38 where
39 TXT: ResolverCache<Box<str>, Txt> + 'x,
40 MXX: ResolverCache<Box<str>, RecordSet<MX>> + 'x,
41 IPV4: ResolverCache<Box<str>, RecordSet<Ipv4Addr>> + 'x,
42 IPV6: ResolverCache<Box<str>, RecordSet<Ipv6Addr>> + 'x,
43 PTR: ResolverCache<IpAddr, RecordSet<Box<str>>> + 'x,
44 {
45 let params = params.into();
46 match ¶ms.params.sender {
47 Sender::Full(sender) => {
48 let helo_output = self
49 .check_host(params.clone_with(SpfParameters::verify_ehlo(
50 params.params.ip,
51 params.params.helo_domain,
52 params.params.host_domain,
53 )))
54 .await;
55 if sender.is_empty() || helo_output.result() == SpfResult::Fail {
56 helo_output
57 } else {
58 self.check_host(params.clone_with(SpfParameters::verify_mail_from(
59 params.params.ip,
60 params.params.helo_domain,
61 params.params.host_domain,
62 sender,
63 )))
64 .await
65 }
66 }
67 _ => self.check_host(params).await,
68 }
69 }
70
71 #[allow(clippy::while_let_on_iterator)]
72 #[allow(clippy::iter_skip_zero)]
73 pub async fn check_host<'x, TXT, MXX, IPV4, IPV6, PTR>(
74 &self,
75 params: Parameters<'x, SpfParameters<'x>, TXT, MXX, IPV4, IPV6, PTR>,
76 ) -> SpfOutput
77 where
78 TXT: ResolverCache<Box<str>, Txt>,
79 MXX: ResolverCache<Box<str>, RecordSet<MX>>,
80 IPV4: ResolverCache<Box<str>, RecordSet<Ipv4Addr>>,
81 IPV6: ResolverCache<Box<str>, RecordSet<Ipv6Addr>>,
82 PTR: ResolverCache<IpAddr, RecordSet<Box<str>>>,
83 {
84 let domain = params.params.domain;
85 let ip = params.params.ip;
86 let helo_domain = params.params.helo_domain;
87 let host_domain = params.params.host_domain;
88 let sender = match ¶ms.params.sender {
89 Sender::Ehlo(sender) => sender.as_str(),
90 Sender::MailFrom(sender) => sender,
91 Sender::Full(sender) => sender,
92 };
93
94 let output = SpfOutput::new(domain.to_string());
95 if domain.is_empty() || domain.len() > 255 || !domain.has_valid_labels() {
96 return output.with_result(SpfResult::None);
97 }
98 let mut vars = Variables::new();
99 let mut has_p_var = false;
100 vars.set_ip(&ip);
101 if !sender.is_empty() {
102 vars.set_sender(sender.as_bytes());
103 } else {
104 vars.set_sender(postmaster_at(domain).into_bytes());
105 }
106 vars.set_domain(domain.as_bytes());
107 vars.set_host_domain(host_domain.as_bytes());
108 vars.set_helo_domain(helo_domain.as_bytes());
109
110 let mut lookup_limit = LookupLimit::new();
111 let mut spf_record = match self.txt_lookup::<Spf>(domain, params.cache_txt).await {
112 Ok(spf_record) => spf_record,
113 Err(err) => return output.with_result(err.into()),
114 };
115
116 let mut domain = Cow::Borrowed(domain);
117 let mut include_stack = Vec::new();
118
119 let mut result = None;
120 let mut directives = spf_record.directives.iter().enumerate().skip(0);
121
122 loop {
123 while let Some((pos, directive)) = directives.next() {
124 if !has_p_var && directive.mechanism.needs_ptr() {
125 if !lookup_limit.can_lookup() {
126 return output
127 .with_result(SpfResult::PermError)
128 .with_report(&spf_record);
129 }
130 if let Some(ptr) = self
131 .ptr_lookup(ip, params.cache_ptr)
132 .await
133 .ok()
134 .and_then(|ptrs| ptrs.rrset.first().map(|ptr| ptr.as_bytes().to_vec()))
135 {
136 vars.set_validated_domain(ptr);
137 }
138 has_p_var = true;
139 }
140
141 let matches = match &directive.mechanism {
142 Mechanism::All => true,
143 Mechanism::Ip4 { addr, mask } => ip.matches_ipv4_mask(addr, *mask),
144 Mechanism::Ip6 { addr, mask } => ip.matches_ipv6_mask(addr, *mask),
145 Mechanism::A {
146 macro_string,
147 ip4_mask,
148 ip6_mask,
149 } => {
150 if !lookup_limit.can_lookup() {
151 return output
152 .with_result(SpfResult::PermError)
153 .with_report(&spf_record);
154 }
155 match self
156 .ip_matches(
157 macro_string.eval(&vars, &domain, true).as_ref(),
158 ip,
159 *ip4_mask,
160 *ip6_mask,
161 params.cache_ipv4,
162 params.cache_ipv6,
163 )
164 .await
165 {
166 Ok(true) => true,
167 Ok(false) | Err(Error::Dns(DnsError::RecordNotFound(_))) => false,
168 Err(_) => {
169 return output
170 .with_result(SpfResult::TempError)
171 .with_report(&spf_record);
172 }
173 }
174 }
175 Mechanism::Mx {
176 macro_string,
177 ip4_mask,
178 ip6_mask,
179 } => {
180 if !lookup_limit.can_lookup() {
181 return output
182 .with_result(SpfResult::PermError)
183 .with_report(&spf_record);
184 }
185
186 let mut matches = false;
187 match self
188 .mx_lookup(&*macro_string.eval(&vars, &domain, true), params.cache_mx)
189 .await
190 {
191 Ok(records) => {
192 for (mx_num, exchange) in records
193 .rrset
194 .iter()
195 .flat_map(|mx| mx.exchanges.iter())
196 .enumerate()
197 {
198 if mx_num > 9 {
199 return output
200 .with_result(SpfResult::PermError)
201 .with_report(&spf_record);
202 }
203
204 match self
205 .ip_matches(
206 exchange,
207 ip,
208 *ip4_mask,
209 *ip6_mask,
210 params.cache_ipv4,
211 params.cache_ipv6,
212 )
213 .await
214 {
215 Ok(true) => {
216 matches = true;
217 break;
218 }
219 Ok(false)
220 | Err(Error::Dns(DnsError::RecordNotFound(_))) => (),
221 Err(_) => {
222 return output
223 .with_result(SpfResult::TempError)
224 .with_report(&spf_record);
225 }
226 }
227 }
228 }
229 Err(Error::Dns(DnsError::RecordNotFound(_))) => (),
230 Err(_) => {
231 return output
232 .with_result(SpfResult::TempError)
233 .with_report(&spf_record);
234 }
235 }
236 matches
237 }
238 Mechanism::Include { macro_string } => {
239 if !lookup_limit.can_lookup() {
240 return output
241 .with_result(SpfResult::PermError)
242 .with_report(&spf_record);
243 }
244
245 let target_name = macro_string.eval(&vars, &domain, true);
246 let included = self
247 .txt_lookup::<Spf>(&*target_name, params.cache_txt)
248 .await;
249 match included {
250 Ok(included_spf) => {
251 let new_domain = target_name.into_owned();
252 include_stack.push((
253 std::mem::replace(&mut spf_record, included_spf),
254 pos,
255 domain,
256 ));
257 directives = spf_record.directives.iter().enumerate().skip(0);
258 vars.set_domain(new_domain.as_bytes().to_vec());
259 domain = Cow::Owned(new_domain);
260 continue;
261 }
262 Err(
263 Error::Dns(DnsError::RecordNotFound(_))
264 | Error::Dns(DnsError::InvalidRecordType)
265 | Error::ParseError,
266 ) => {
267 return output
268 .with_result(SpfResult::PermError)
269 .with_report(&spf_record);
270 }
271 Err(_) => {
272 return output
273 .with_result(SpfResult::TempError)
274 .with_report(&spf_record);
275 }
276 }
277 }
278 Mechanism::Ptr { macro_string } => {
279 if !lookup_limit.can_lookup() {
280 return output
281 .with_result(SpfResult::PermError)
282 .with_report(&spf_record);
283 }
284
285 let target_name = macro_string.eval(&vars, &domain, true);
286 let target_addr = to_lowercase(target_name.as_ref());
287 let target_addr = target_addr.as_ref();
288 let mut matches = false;
289
290 if let Ok(records) = self.ptr_lookup(ip, params.cache_ptr).await {
291 for record in records.rrset.iter() {
292 if lookup_limit.can_lookup()
293 && let Ok(true) = self
294 .ip_matches(
295 record,
296 ip,
297 u32::MAX,
298 u128::MAX,
299 params.cache_ipv4,
300 params.cache_ipv6,
301 )
302 .await
303 {
304 matches = record.as_ref() == target_addr
305 || record
306 .strip_suffix('.')
307 .unwrap_or(record.as_ref())
308 .strip_suffix(target_addr)
309 .is_some_and(|prefix| prefix.ends_with('.'));
310 if matches {
311 break;
312 }
313 }
314 }
315 }
316 matches
317 }
318 Mechanism::Exists { macro_string } => {
319 if !lookup_limit.can_lookup() {
320 return output
321 .with_result(SpfResult::PermError)
322 .with_report(&spf_record);
323 }
324
325 if let Ok(result) = self
326 .exists(
327 &*macro_string.eval(&vars, &domain, true),
328 params.cache_ipv4,
329 params.cache_ipv6,
330 )
331 .await
332 {
333 result
334 } else {
335 return output
336 .with_result(SpfResult::TempError)
337 .with_report(&spf_record);
338 }
339 }
340 };
341
342 if matches {
343 result = Some((&directive.qualifier).into());
344 break;
345 }
346 }
347
348 if let (Some(macro_string), None) = (&spf_record.redirect, &result) {
350 if !lookup_limit.can_lookup() {
351 return output
352 .with_result(SpfResult::PermError)
353 .with_report(&spf_record);
354 }
355
356 let target_name = macro_string.eval(&vars, &domain, true);
357 let redirect = self
358 .txt_lookup::<Spf>(&*target_name, params.cache_txt)
359 .await;
360 match redirect {
361 Ok(redirect_spf) => {
362 let new_domain = target_name.into_owned();
363 spf_record = redirect_spf;
364 directives = spf_record.directives.iter().enumerate().skip(0);
365 vars.set_domain(new_domain.as_bytes().to_vec());
366 domain = Cow::Owned(new_domain);
367 continue;
368 }
369 Err(
370 Error::Dns(DnsError::RecordNotFound(_))
371 | Error::Dns(DnsError::InvalidRecordType)
372 | Error::ParseError,
373 ) => {
374 return output
375 .with_result(SpfResult::PermError)
376 .with_report(&spf_record);
377 }
378 Err(_) => {
379 return output
380 .with_result(SpfResult::TempError)
381 .with_report(&spf_record);
382 }
383 }
384 }
385
386 if let Some((prev_record, prev_pos, prev_domain)) = include_stack.pop() {
387 spf_record = prev_record;
388 directives = spf_record.directives.iter().enumerate().skip(prev_pos);
389 let qualifier = directives.next().map(|(_, directive)| &directive.qualifier);
390
391 if matches!(result, Some(SpfResult::Pass)) {
392 if let Some(qualifier) = qualifier {
393 result = Some(qualifier.into());
394 }
395 break;
396 } else {
397 vars.set_domain(prev_domain.as_bytes().to_vec());
398 domain = prev_domain;
399 result = None;
400 }
401 } else {
402 break;
403 }
404 }
405
406 if let (Some(macro_string), Some(SpfResult::Fail)) = (&spf_record.exp, &result)
408 && let Ok(macro_string) = self
409 .txt_lookup::<Macro>(macro_string.eval(&vars, &domain, true), params.cache_txt)
410 .await
411 {
412 return output
413 .with_result(SpfResult::Fail)
414 .with_explanation(macro_string.eval(&vars, &domain, false).into_owned())
415 .with_report(&spf_record);
416 }
417
418 output
419 .with_result(result.unwrap_or(SpfResult::Neutral))
420 .with_report(&spf_record)
421 }
422
423 async fn ip_matches(
424 &self,
425 target_name: &str,
426 ip: IpAddr,
427 ip4_mask: u32,
428 ip6_mask: u128,
429 cache_ipv4: Option<&impl ResolverCache<Box<str>, RecordSet<Ipv4Addr>>>,
430 cache_ipv6: Option<&impl ResolverCache<Box<str>, RecordSet<Ipv6Addr>>>,
431 ) -> crate::Result<bool> {
432 Ok(match ip {
433 IpAddr::V4(ip) => self
434 .ipv4_lookup(target_name, cache_ipv4)
435 .await?
436 .rrset
437 .iter()
438 .any(|addr| ip.matches_ipv4_mask(addr, ip4_mask)),
439 IpAddr::V6(ip) => self
440 .ipv6_lookup(target_name, cache_ipv6)
441 .await?
442 .rrset
443 .iter()
444 .any(|addr| ip.matches_ipv6_mask(addr, ip6_mask)),
445 })
446 }
447}
448
449fn postmaster_at(domain: &str) -> String {
450 const POSTMASTER: &str = "postmaster@";
451 let mut sender = String::with_capacity(POSTMASTER.len() + domain.len());
452 sender.push_str(POSTMASTER);
453 sender.push_str(domain);
454 sender
455}
456
457fn to_lowercase(value: &str) -> Cow<'_, str> {
458 if value.is_ascii() {
459 if value.bytes().any(|byte| byte.is_ascii_uppercase()) {
460 Cow::Owned(value.to_ascii_lowercase())
461 } else {
462 Cow::Borrowed(value)
463 }
464 } else {
465 Cow::Owned(value.to_lowercase())
466 }
467}
468
469impl<'x> SpfParameters<'x> {
470 pub fn verify_ehlo(
472 ip: IpAddr,
473 helo_domain: &'x str,
474 host_domain: &'x str,
475 ) -> SpfParameters<'x> {
476 SpfParameters {
477 ip,
478 domain: helo_domain,
479 helo_domain,
480 host_domain,
481 sender: Sender::Ehlo(postmaster_at(helo_domain)),
482 }
483 }
484
485 pub fn verify_mail_from(
487 ip: IpAddr,
488 helo_domain: &'x str,
489 host_domain: &'x str,
490 sender: &'x str,
491 ) -> SpfParameters<'x> {
492 SpfParameters {
493 ip,
494 domain: sender.rsplit_once('@').map_or(helo_domain, |(_, d)| d),
495 helo_domain,
496 host_domain,
497 sender: Sender::MailFrom(sender),
498 }
499 }
500
501 pub fn verify(
503 ip: IpAddr,
504 helo_domain: &'x str,
505 host_domain: &'x str,
506 sender: &'x str,
507 ) -> SpfParameters<'x> {
508 SpfParameters {
509 ip,
510 domain: sender.rsplit_once('@').map_or(helo_domain, |(_, d)| d),
511 helo_domain,
512 host_domain,
513 sender: Sender::Full(sender),
514 }
515 }
516
517 pub fn new(
518 ip: IpAddr,
519 domain: &'x str,
520 helo_domain: &'x str,
521 host_domain: &'x str,
522 sender: &'x str,
523 ) -> Self {
524 SpfParameters {
525 ip,
526 domain,
527 helo_domain,
528 host_domain,
529 sender: Sender::Full(sender),
530 }
531 }
532}
533
534impl<'x> From<SpfParameters<'x>>
535 for Parameters<
536 'x,
537 SpfParameters<'x>,
538 NoCache<Box<str>, Txt>,
539 NoCache<Box<str>, RecordSet<MX>>,
540 NoCache<Box<str>, RecordSet<Ipv4Addr>>,
541 NoCache<Box<str>, RecordSet<Ipv6Addr>>,
542 NoCache<IpAddr, RecordSet<Box<str>>>,
543 >
544{
545 fn from(params: SpfParameters<'x>) -> Self {
546 Parameters::new(params)
547 }
548}
549
550trait IpMask {
551 fn matches_ipv4_mask(&self, addr: &Ipv4Addr, mask: u32) -> bool;
552 fn matches_ipv6_mask(&self, addr: &Ipv6Addr, mask: u128) -> bool;
553}
554
555impl IpMask for IpAddr {
556 fn matches_ipv4_mask(&self, addr: &Ipv4Addr, mask: u32) -> bool {
557 u32::from_be_bytes(match &self {
558 IpAddr::V4(ip) => ip.octets(),
559 IpAddr::V6(ip) => {
560 if let Some(ip) = ip.to_ipv4_mapped() {
561 ip.octets()
562 } else {
563 return false;
564 }
565 }
566 }) & mask
567 == u32::from_be_bytes(addr.octets()) & mask
568 }
569
570 fn matches_ipv6_mask(&self, addr: &Ipv6Addr, mask: u128) -> bool {
571 u128::from_be_bytes(match &self {
572 IpAddr::V6(ip) => ip.octets(),
573 IpAddr::V4(ip) => ip.to_ipv6_mapped().octets(),
574 }) & mask
575 == u128::from_be_bytes(addr.octets()) & mask
576 }
577}
578
579impl IpMask for Ipv6Addr {
580 fn matches_ipv6_mask(&self, addr: &Ipv6Addr, mask: u128) -> bool {
581 u128::from_be_bytes(self.octets()) & mask == u128::from_be_bytes(addr.octets()) & mask
582 }
583
584 fn matches_ipv4_mask(&self, _addr: &Ipv4Addr, _mask: u32) -> bool {
585 unimplemented!()
586 }
587}
588
589impl IpMask for Ipv4Addr {
590 fn matches_ipv4_mask(&self, addr: &Ipv4Addr, mask: u32) -> bool {
591 u32::from_be_bytes(self.octets()) & mask == u32::from_be_bytes(addr.octets()) & mask
592 }
593
594 fn matches_ipv6_mask(&self, _addr: &Ipv6Addr, _mask: u128) -> bool {
595 unimplemented!()
596 }
597}
598
599impl From<&Qualifier> for SpfResult {
600 fn from(q: &Qualifier) -> Self {
601 match q {
602 Qualifier::Pass => SpfResult::Pass,
603 Qualifier::Fail => SpfResult::Fail,
604 Qualifier::SoftFail => SpfResult::SoftFail,
605 Qualifier::Neutral => SpfResult::Neutral,
606 }
607 }
608}
609
610impl From<Error> for SpfResult {
611 fn from(err: Error) -> Self {
612 match err {
613 Error::Dns(DnsError::RecordNotFound(_)) | Error::Dns(DnsError::InvalidRecordType) => {
614 SpfResult::None
615 }
616 Error::ParseError => SpfResult::PermError,
617 _ => SpfResult::TempError,
618 }
619 }
620}
621
622struct LookupLimit {
623 num_lookups: u32,
624 timer: Instant,
625}
626
627impl LookupLimit {
628 pub fn new() -> Self {
629 LookupLimit {
630 num_lookups: 1,
631 timer: Instant::now(),
632 }
633 }
634
635 #[inline(always)]
636 fn can_lookup(&mut self) -> bool {
637 if self.num_lookups <= 10 && self.timer.elapsed().as_secs() < 20 {
638 self.num_lookups += 1;
639 true
640 } else {
641 false
642 }
643 }
644}
645
646pub trait HasValidLabels {
647 fn has_valid_labels(&self) -> bool;
648}
649
650impl HasValidLabels for &str {
651 fn has_valid_labels(&self) -> bool {
652 let mut has_dots = false;
653 let mut has_chars = false;
654 let mut label_len = 0;
655 for ch in self.chars() {
656 label_len += 1;
657
658 if ch.is_alphanumeric() {
659 has_chars = true;
660 } else if ch == '.' {
661 has_dots = true;
662 label_len = 0;
663 }
664
665 if label_len > 63 {
666 return false;
667 }
668 }
669 if has_chars && has_dots {
670 return true;
671 }
672 false
673 }
674}
675
676#[cfg(test)]
677#[allow(unused)]
678mod test {
679
680 use std::{
681 fs,
682 net::{IpAddr, Ipv4Addr, Ipv6Addr},
683 path::PathBuf,
684 time::{Duration, Instant},
685 };
686
687 use crate::{
688 MX, MessageAuthenticator, SpfResult,
689 common::{cache::test::DummyCaches, parse::TxtRecordParser},
690 spf::{Macro, Spf},
691 };
692
693 use super::SpfParameters;
694
695 #[tokio::test]
696 async fn spf_verify() {
697 let resolver = MessageAuthenticator::new_system_conf().unwrap();
698 let valid_until = Instant::now() + Duration::from_secs(30);
699 let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
700 test_dir.push("resources");
701 test_dir.push("spf");
702
703 for file_name in fs::read_dir(&test_dir).unwrap() {
704 let file_name = file_name.unwrap().path();
705 println!("===== {} =====", file_name.display());
706 let test_suite = String::from_utf8(fs::read(&file_name).unwrap()).unwrap();
707 let caches = DummyCaches::new();
708
709 for test in test_suite.split("---\n") {
710 let mut test_name = "";
711 let mut last_test_name = "";
712 let mut helo = "";
713 let mut mail_from = "";
714 let mut client_ip = "127.0.0.1".parse::<IpAddr>().unwrap();
715 let mut test_num = 1;
716
717 for line in test.split('\n') {
718 let line = line.trim();
719 let line = if let Some(line) = line.strip_prefix('-') {
720 line.trim()
721 } else {
722 line
723 };
724
725 if let Some(name) = line.strip_prefix("name:") {
726 test_name = name.trim();
727 } else if let Some(record) = line.strip_prefix("spf:") {
728 let (name, record) = record.trim().split_once(' ').unwrap();
729 caches.txt_add(
730 name.trim().to_string(),
731 Spf::parse(record.as_bytes()),
732 valid_until,
733 );
734 } else if let Some(record) = line.strip_prefix("exp:") {
735 let (name, record) = record.trim().split_once(' ').unwrap();
736 caches.txt_add(
737 name.trim().to_string(),
738 Macro::parse(record.as_bytes()),
739 valid_until,
740 );
741 } else if let Some(record) = line.strip_prefix("a:") {
742 let (name, record) = record.trim().split_once(' ').unwrap();
743 caches.ipv4_add(
744 name.trim().to_string(),
745 record
746 .split(',')
747 .map(|item| item.trim().parse::<Ipv4Addr>().unwrap())
748 .collect(),
749 valid_until,
750 );
751 } else if let Some(record) = line.strip_prefix("aaaa:") {
752 let (name, record) = record.trim().split_once(' ').unwrap();
753 caches.ipv6_add(
754 name.trim().to_string(),
755 record
756 .split(',')
757 .map(|item| item.trim().parse::<Ipv6Addr>().unwrap())
758 .collect(),
759 valid_until,
760 );
761 } else if let Some(record) = line.strip_prefix("ptr:") {
762 let (name, record) = record.trim().split_once(' ').unwrap();
763 caches.ptr_add(
764 name.trim().parse::<IpAddr>().unwrap(),
765 record
766 .split(',')
767 .map(|item| Box::from(item.trim()))
768 .collect(),
769 valid_until,
770 );
771 } else if let Some(record) = line.strip_prefix("mx:") {
772 let (name, record) = record.trim().split_once(' ').unwrap();
773 let mut mxs = Vec::new();
774 for (pos, item) in record.split(',').enumerate() {
775 let ip = item.trim().parse::<IpAddr>().unwrap();
776 let mx_name = format!("mx.{ip}.{pos}");
777 match ip {
778 IpAddr::V4(ip) => {
779 caches.ipv4_add(mx_name.clone(), vec![ip], valid_until)
780 }
781 IpAddr::V6(ip) => {
782 caches.ipv6_add(mx_name.clone(), vec![ip], valid_until)
783 }
784 }
785 mxs.push(MX {
786 exchanges: Box::new([mx_name.into_boxed_str()]),
787 preference: (pos + 1) as u16,
788 });
789 }
790 caches.mx_add(name.trim().to_string(), mxs, valid_until);
791 } else if let Some(value) = line.strip_prefix("domain:") {
792 helo = value.trim();
793 } else if let Some(value) = line.strip_prefix("sender:") {
794 mail_from = value.trim();
795 } else if let Some(value) = line.strip_prefix("ip:") {
796 client_ip = value.trim().parse().unwrap();
797 } else if let Some(value) = line.strip_prefix("expect:") {
798 let value = value.trim();
799 let (result, exp): (SpfResult, &str) =
800 if let Some((result, exp)) = value.split_once(' ') {
801 (result.trim().try_into().unwrap(), exp.trim())
802 } else {
803 (value.try_into().unwrap(), "")
804 };
805 let output = resolver
806 .verify_spf(caches.parameters(SpfParameters::verify(
807 client_ip,
808 helo,
809 "localdomain.org",
810 mail_from,
811 )))
812 .await;
813 assert_eq!(
814 output.result(),
815 result,
816 "Failed for {test_name:?}, test {test_num}, ehlo: {helo}, mail-from: {mail_from}.",
817 );
818
819 if !exp.is_empty() {
820 assert_eq!(Some(exp.to_string()).as_deref(), output.explanation());
821 }
822 test_num += 1;
823 if test_name != last_test_name {
824 println!("Passed test {test_name:?}");
825 last_test_name = test_name;
826 }
827 }
828 }
829 }
830 }
831 }
832}