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