Skip to main content

mail_auth/spf/
verify.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7use 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    /// Verifies the SPF record of a domain
34    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 &params.params.sender {
47            Sender::Full(sender) => {
48                // Verify HELO identity
49                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                    // Verify MAIL FROM identity
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                } 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 &params.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            // Follow redirect
351            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        // Evaluate explain
409        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    /// Verifies the SPF EHLO identity
473    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    /// Verifies the SPF MAIL FROM identity
488    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    /// Verifies both the SPF EHLO and MAIL FROM identities
504    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}