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                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 &params.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            // Follow redirect
349            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        // Evaluate explain
407        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    /// Verifies the SPF EHLO identity
471    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    /// Verifies the SPF MAIL FROM identity
486    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    /// Verifies both the SPF EHLO and MAIL FROM identities
502    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}