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, SpfIdentity, SpfRecord, Variables};
8use crate::DnsError;
9use crate::Instant;
10use crate::dns::{DnsCache, Negative, has_valid_labels};
11use crate::{
12    Error, MessageAuthenticator, Parameters, RecordSet, ResolverCache, SpfOutput, SpfResult,
13    dns::cache::NoCache,
14};
15use std::borrow::Cow;
16use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
17
18/// Input of an SPF check: the client IP, the identity to check and the
19/// values used for macro expansion.
20///
21/// Built with [`SpfParameters::helo`], [`SpfParameters::mail_from`],
22/// [`SpfParameters::helo_and_mail_from`] or [`SpfParameters::new`], and
23/// passed to [`MessageAuthenticator::verify_spf`] or
24/// [`MessageAuthenticator::check_host`], optionally wrapped in [`Parameters`]
25/// to supply a DNS cache.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct SpfParameters<'x> {
28    ip: IpAddr,
29    domain: &'x str,
30    helo_domain: &'x str,
31    host_domain: &'x str,
32    sender: Sender<'x>,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36enum Sender<'x> {
37    Helo,
38    MailFrom(&'x str),
39    Full(&'x str),
40}
41
42#[allow(clippy::iter_skip_zero)]
43impl MessageAuthenticator {
44    /// Verifies the SPF identities of an SMTP session (RFC 7208).
45    ///
46    /// With parameters built by [`SpfParameters::helo_and_mail_from`] (or
47    /// [`SpfParameters::new`]), the HELO identity is checked first. Its output
48    /// is returned when the result is `fail` or the sender is empty;
49    /// otherwise the MAIL FROM identity is checked and its output returned.
50    /// With parameters built by [`SpfParameters::helo`] or
51    /// [`SpfParameters::mail_from`], a single [`check_host`] is run for that
52    /// identity.
53    ///
54    /// The DNS lookups performed are those of [`check_host`], once per
55    /// identity checked. See [`SpfResult`] for the meaning of each result.
56    ///
57    /// # Example
58    ///
59    /// ```rust,no_run
60    /// use mail_auth::{MessageAuthenticator, SpfResult, spf::verify::SpfParameters};
61    ///
62    /// # async fn run(authenticator: &MessageAuthenticator) {
63    /// let output = authenticator
64    ///     .verify_spf(SpfParameters::helo_and_mail_from(
65    ///         "192.0.2.1".parse().unwrap(),
66    ///         "mx.example.org",
67    ///         "mx.my-host.org",
68    ///         "sender@example.org",
69    ///     ))
70    ///     .await;
71    ///
72    /// if output.result() == SpfResult::Fail {
73    ///     println!("rejected: {:?}", output.explanation());
74    /// }
75    /// # }
76    /// ```
77    ///
78    /// [`check_host`]: MessageAuthenticator::check_host
79    pub async fn verify_spf<'x, C>(
80        &self,
81        params: impl Into<Parameters<'x, SpfParameters<'x>, C>>,
82    ) -> SpfOutput
83    where
84        C: DnsCache + 'x,
85    {
86        let params = params.into();
87        match &params.input.sender {
88            Sender::Full(sender) => {
89                let helo_output = self
90                    .check_host(params.clone_with(SpfParameters::helo(
91                        params.input.ip,
92                        params.input.helo_domain,
93                        params.input.host_domain,
94                    )))
95                    .await;
96                if sender.is_empty() || helo_output.result() == SpfResult::Fail {
97                    helo_output
98                } else {
99                    self.check_host(params.clone_with(SpfParameters::mail_from(
100                        params.input.ip,
101                        params.input.helo_domain,
102                        params.input.host_domain,
103                        sender,
104                    )))
105                    .await
106                }
107            }
108            _ => self.check_host(params).await,
109        }
110    }
111
112    /// Runs the `check_host()` function (RFC 7208, Section 4) for a single
113    /// identity.
114    ///
115    /// Evaluates the SPF record of the parameters' domain against the client
116    /// IP. The DNS lookups performed are:
117    ///
118    /// - TXT for the `v=spf1` record of the domain, and of every `include:`
119    ///   and `redirect=` target.
120    /// - A or AAAA (depending on the client IP family) for the `a`, `mx`,
121    ///   `ptr` and `exists` mechanisms.
122    /// - MX for the `mx` mechanism.
123    /// - PTR for the `ptr` mechanism and the `p` macro.
124    /// - TXT for the `exp=` explanation, only on a `fail` result.
125    ///
126    /// The number of DNS-querying mechanisms and modifiers is limited to 10
127    /// and the whole evaluation to 20 seconds; an `mx` mechanism may resolve
128    /// at most 10 exchanges. Exceeding a limit yields `permerror`.
129    ///
130    /// The returned [`SpfOutput`] holds one of:
131    ///
132    /// - [`SpfResult::None`]: the domain is not a valid name or publishes no
133    ///   SPF record.
134    /// - [`SpfResult::Pass`], [`SpfResult::Fail`], [`SpfResult::SoftFail`] or
135    ///   [`SpfResult::Neutral`]: the qualifier of the first matching
136    ///   directive, or `neutral` when none matches.
137    /// - [`SpfResult::TempError`]: a DNS lookup failed transiently.
138    /// - [`SpfResult::PermError`]: a record could not be parsed, an
139    ///   `include:` or `redirect=` target has no record, or a limit was
140    ///   exceeded.
141    ///
142    /// # Example
143    ///
144    /// ```rust,no_run
145    /// use mail_auth::{MessageAuthenticator, spf::verify::SpfParameters};
146    ///
147    /// # async fn run(authenticator: &MessageAuthenticator) {
148    /// let output = authenticator
149    ///     .check_host(SpfParameters::new(
150    ///         "192.0.2.1".parse().unwrap(),
151    ///         "example.org",
152    ///         "mx.example.org",
153    ///         "mx.my-host.org",
154    ///         "sender@example.org",
155    ///     ))
156    ///     .await;
157    /// println!("spf={} domain={}", output.result(), output.domain());
158    /// # }
159    /// ```
160    #[allow(clippy::while_let_on_iterator)]
161    #[allow(clippy::iter_skip_zero)]
162    pub async fn check_host<'x, C>(
163        &self,
164        params: impl Into<Parameters<'x, SpfParameters<'x>, C>>,
165    ) -> SpfOutput
166    where
167        C: DnsCache + 'x,
168    {
169        let params = params.into();
170        let domain = params.input.domain;
171        let ip = params.input.ip;
172        let helo_domain = params.input.helo_domain;
173        let host_domain = params.input.host_domain;
174        let (sender, identity) = match &params.input.sender {
175            Sender::Helo => ("", SpfIdentity::Helo),
176            Sender::MailFrom(sender) | Sender::Full(sender) => (*sender, SpfIdentity::MailFrom),
177        };
178
179        let output = SpfOutput::new(domain.to_string()).with_identity(identity);
180        if domain.is_empty() || domain.len() > 255 || !has_valid_labels(domain) {
181            return output.with_result(SpfResult::None);
182        }
183        let postmaster;
184        let sender = if sender.is_empty() {
185            postmaster = postmaster_at(domain);
186            postmaster.as_str()
187        } else {
188            sender
189        };
190        let mut vars = Variables::new();
191        let mut has_p_var = false;
192        vars.set_ip(&ip);
193        vars.set_sender(sender.as_bytes());
194        vars.set_domain(domain.as_bytes());
195        vars.set_host_domain(host_domain.as_bytes());
196        vars.set_helo_domain(helo_domain.as_bytes());
197
198        let mut lookup_limit = LookupLimit::new();
199        let mut spf_record = match self
200            .txt_lookup::<SpfRecord>(domain, params.txt_cache())
201            .await
202        {
203            Ok(spf_record) => spf_record,
204            Err(err) => return output.with_result(err.into()),
205        };
206
207        let mut domain = Cow::Borrowed(domain);
208        let mut include_stack = Vec::new();
209
210        let mut result = None;
211        let mut directives = spf_record.directives.iter().enumerate().skip(0);
212
213        loop {
214            while let Some((pos, directive)) = directives.next() {
215                if !has_p_var && directive.mechanism.needs_ptr() {
216                    if !lookup_limit.can_lookup() {
217                        return output
218                            .with_result(SpfResult::PermError)
219                            .with_report(&spf_record);
220                    }
221                    if let Some(ptr) = self
222                        .ptr_lookup(ip, params.ptr_cache())
223                        .await
224                        .ok()
225                        .and_then(|ptrs| ptrs.records.first().map(|ptr| ptr.as_bytes().to_vec()))
226                    {
227                        vars.set_validated_domain(ptr);
228                    }
229                    has_p_var = true;
230                }
231
232                let matches = match &directive.mechanism {
233                    Mechanism::All => true,
234                    Mechanism::Ip4 { addr, mask } => ip.matches_ipv4_mask(addr, *mask),
235                    Mechanism::Ip6 { addr, mask } => ip.matches_ipv6_mask(addr, *mask),
236                    Mechanism::A {
237                        macro_string,
238                        ip4_mask,
239                        ip6_mask,
240                    } => {
241                        if !lookup_limit.can_lookup() {
242                            return output
243                                .with_result(SpfResult::PermError)
244                                .with_report(&spf_record);
245                        }
246                        match self
247                            .ip_matches(
248                                macro_string.eval(&vars, &domain, true).as_ref(),
249                                ip,
250                                *ip4_mask,
251                                *ip6_mask,
252                                params.ipv4_cache(),
253                                params.ipv6_cache(),
254                            )
255                            .await
256                        {
257                            Ok(true) => true,
258                            Ok(false) | Err(Error::Dns(DnsError::RecordNotFound(_))) => false,
259                            Err(_) => {
260                                return output
261                                    .with_result(SpfResult::TempError)
262                                    .with_report(&spf_record);
263                            }
264                        }
265                    }
266                    Mechanism::Mx {
267                        macro_string,
268                        ip4_mask,
269                        ip6_mask,
270                    } => {
271                        if !lookup_limit.can_lookup() {
272                            return output
273                                .with_result(SpfResult::PermError)
274                                .with_report(&spf_record);
275                        }
276
277                        let mut matches = false;
278                        match self
279                            .mx_lookup(&*macro_string.eval(&vars, &domain, true), params.mx_cache())
280                            .await
281                        {
282                            Ok(records) => {
283                                for (mx_num, exchange) in records
284                                    .records
285                                    .iter()
286                                    .flat_map(|mx| mx.exchanges.iter())
287                                    .enumerate()
288                                {
289                                    if mx_num > 9 {
290                                        return output
291                                            .with_result(SpfResult::PermError)
292                                            .with_report(&spf_record);
293                                    }
294
295                                    match self
296                                        .ip_matches(
297                                            exchange,
298                                            ip,
299                                            *ip4_mask,
300                                            *ip6_mask,
301                                            params.ipv4_cache(),
302                                            params.ipv6_cache(),
303                                        )
304                                        .await
305                                    {
306                                        Ok(true) => {
307                                            matches = true;
308                                            break;
309                                        }
310                                        Ok(false)
311                                        | Err(Error::Dns(DnsError::RecordNotFound(_))) => (),
312                                        Err(_) => {
313                                            return output
314                                                .with_result(SpfResult::TempError)
315                                                .with_report(&spf_record);
316                                        }
317                                    }
318                                }
319                            }
320                            Err(Error::Dns(DnsError::RecordNotFound(_))) => (),
321                            Err(_) => {
322                                return output
323                                    .with_result(SpfResult::TempError)
324                                    .with_report(&spf_record);
325                            }
326                        }
327                        matches
328                    }
329                    Mechanism::Include { macro_string } => {
330                        if !lookup_limit.can_lookup() {
331                            return output
332                                .with_result(SpfResult::PermError)
333                                .with_report(&spf_record);
334                        }
335
336                        let target_name = macro_string.eval(&vars, &domain, true);
337                        let included = self
338                            .txt_lookup::<SpfRecord>(&*target_name, params.txt_cache())
339                            .await;
340                        match included {
341                            Ok(included_spf) => {
342                                let new_domain = target_name.into_owned();
343                                include_stack.push((
344                                    std::mem::replace(&mut spf_record, included_spf),
345                                    pos,
346                                    domain,
347                                ));
348                                directives = spf_record.directives.iter().enumerate().skip(0);
349                                vars.set_domain(new_domain.as_bytes().to_vec());
350                                domain = Cow::Owned(new_domain);
351                                continue;
352                            }
353                            Err(
354                                Error::Dns(DnsError::RecordNotFound(_))
355                                | Error::Dns(DnsError::InvalidRecordType)
356                                | Error::Parse,
357                            ) => {
358                                return output
359                                    .with_result(SpfResult::PermError)
360                                    .with_report(&spf_record);
361                            }
362                            Err(_) => {
363                                return output
364                                    .with_result(SpfResult::TempError)
365                                    .with_report(&spf_record);
366                            }
367                        }
368                    }
369                    Mechanism::Ptr { macro_string } => {
370                        if !lookup_limit.can_lookup() {
371                            return output
372                                .with_result(SpfResult::PermError)
373                                .with_report(&spf_record);
374                        }
375
376                        let target_name = macro_string.eval(&vars, &domain, true);
377                        let target_addr = to_lowercase(target_name.as_ref());
378                        let target_addr = target_addr.as_ref();
379                        let mut matches = false;
380
381                        if let Ok(records) = self.ptr_lookup(ip, params.ptr_cache()).await {
382                            for record in records.records.iter() {
383                                if lookup_limit.can_lookup()
384                                    && let Ok(true) = self
385                                        .ip_matches(
386                                            record,
387                                            ip,
388                                            u32::MAX,
389                                            u128::MAX,
390                                            params.ipv4_cache(),
391                                            params.ipv6_cache(),
392                                        )
393                                        .await
394                                {
395                                    matches = record.as_ref() == target_addr
396                                        || record
397                                            .strip_suffix('.')
398                                            .unwrap_or(record.as_ref())
399                                            .strip_suffix(target_addr)
400                                            .is_some_and(|prefix| prefix.ends_with('.'));
401                                    if matches {
402                                        break;
403                                    }
404                                }
405                            }
406                        }
407                        matches
408                    }
409                    Mechanism::Exists { macro_string } => {
410                        if !lookup_limit.can_lookup() {
411                            return output
412                                .with_result(SpfResult::PermError)
413                                .with_report(&spf_record);
414                        }
415
416                        if let Ok(result) = self
417                            .exists(
418                                &*macro_string.eval(&vars, &domain, true),
419                                params.ipv4_cache(),
420                                params.ipv6_cache(),
421                            )
422                            .await
423                        {
424                            result
425                        } else {
426                            return output
427                                .with_result(SpfResult::TempError)
428                                .with_report(&spf_record);
429                        }
430                    }
431                };
432
433                if matches {
434                    result = Some((&directive.qualifier).into());
435                    break;
436                }
437            }
438
439            if let (Some(macro_string), None) = (&spf_record.redirect, &result) {
440                if !lookup_limit.can_lookup() {
441                    return output
442                        .with_result(SpfResult::PermError)
443                        .with_report(&spf_record);
444                }
445
446                let target_name = macro_string.eval(&vars, &domain, true);
447                let redirect = self
448                    .txt_lookup::<SpfRecord>(&*target_name, params.txt_cache())
449                    .await;
450                match redirect {
451                    Ok(redirect_spf) => {
452                        let new_domain = target_name.into_owned();
453                        spf_record = redirect_spf;
454                        directives = spf_record.directives.iter().enumerate().skip(0);
455                        vars.set_domain(new_domain.as_bytes().to_vec());
456                        domain = Cow::Owned(new_domain);
457                        continue;
458                    }
459                    Err(
460                        Error::Dns(DnsError::RecordNotFound(_))
461                        | Error::Dns(DnsError::InvalidRecordType)
462                        | Error::Parse,
463                    ) => {
464                        return output
465                            .with_result(SpfResult::PermError)
466                            .with_report(&spf_record);
467                    }
468                    Err(_) => {
469                        return output
470                            .with_result(SpfResult::TempError)
471                            .with_report(&spf_record);
472                    }
473                }
474            }
475
476            if let Some((prev_record, prev_pos, prev_domain)) = include_stack.pop() {
477                spf_record = prev_record;
478                directives = spf_record.directives.iter().enumerate().skip(prev_pos);
479                let qualifier = directives.next().map(|(_, directive)| &directive.qualifier);
480
481                if matches!(result, Some(SpfResult::Pass)) {
482                    if let Some(qualifier) = qualifier {
483                        result = Some(qualifier.into());
484                    }
485                    break;
486                } else {
487                    vars.set_domain(prev_domain.as_bytes().to_vec());
488                    domain = prev_domain;
489                    result = None;
490                }
491            } else {
492                break;
493            }
494        }
495
496        if let (Some(macro_string), Some(SpfResult::Fail)) = (&spf_record.exp, &result)
497            && let Ok(macro_string) = self
498                .txt_lookup::<Macro>(macro_string.eval(&vars, &domain, true), params.txt_cache())
499                .await
500        {
501            return output
502                .with_result(SpfResult::Fail)
503                .with_explanation(macro_string.eval(&vars, &domain, false).into_owned())
504                .with_report(&spf_record);
505        }
506
507        output
508            .with_result(result.unwrap_or(SpfResult::Neutral))
509            .with_report(&spf_record)
510    }
511
512    async fn ip_matches(
513        &self,
514        target_name: &str,
515        ip: IpAddr,
516        ip4_mask: u32,
517        ip6_mask: u128,
518        cache_ipv4: Option<&impl ResolverCache<Box<str>, Result<RecordSet<Ipv4Addr>, Negative>>>,
519        cache_ipv6: Option<&impl ResolverCache<Box<str>, Result<RecordSet<Ipv6Addr>, Negative>>>,
520    ) -> crate::Result<bool> {
521        Ok(match ip {
522            IpAddr::V4(ip) => self
523                .ipv4_lookup(target_name, cache_ipv4)
524                .await?
525                .records
526                .iter()
527                .any(|addr| ip.matches_ipv4_mask(addr, ip4_mask)),
528            IpAddr::V6(ip) => self
529                .ipv6_lookup(target_name, cache_ipv6)
530                .await?
531                .records
532                .iter()
533                .any(|addr| ip.matches_ipv6_mask(addr, ip6_mask)),
534        })
535    }
536}
537
538fn postmaster_at(domain: &str) -> String {
539    const POSTMASTER: &str = "postmaster@";
540    let mut sender = String::with_capacity(POSTMASTER.len() + domain.len());
541    sender.push_str(POSTMASTER);
542    sender.push_str(domain);
543    sender
544}
545
546fn to_lowercase(value: &str) -> Cow<'_, str> {
547    if value.is_ascii() {
548        if value.bytes().any(|byte| byte.is_ascii_uppercase()) {
549            Cow::Owned(value.to_ascii_lowercase())
550        } else {
551            Cow::Borrowed(value)
552        }
553    } else {
554        Cow::Owned(value.to_lowercase())
555    }
556}
557
558impl<'x> SpfParameters<'x> {
559    /// Checks the HELO identity (RFC 7208, Section 2.3).
560    ///
561    /// The SPF record of `helo_domain` is evaluated with `<sender>` set to
562    /// `postmaster@<helo_domain>`. `host_domain` is the name of the host
563    /// performing the check (the `r` macro).
564    pub fn helo(ip: IpAddr, helo_domain: &'x str, host_domain: &'x str) -> SpfParameters<'x> {
565        SpfParameters {
566            ip,
567            domain: helo_domain,
568            helo_domain,
569            host_domain,
570            sender: Sender::Helo,
571        }
572    }
573
574    /// Checks the MAIL FROM identity (RFC 7208, Section 2.4).
575    ///
576    /// The SPF record of the domain of `sender` is evaluated; when `sender`
577    /// has no `@`, `helo_domain` is used instead. An empty `sender` (null
578    /// reverse-path) is replaced by `postmaster@<domain>`. `host_domain` is
579    /// the name of the host performing the check (the `r` macro).
580    pub fn mail_from(
581        ip: IpAddr,
582        helo_domain: &'x str,
583        host_domain: &'x str,
584        sender: &'x str,
585    ) -> SpfParameters<'x> {
586        SpfParameters {
587            ip,
588            domain: sender.rsplit_once('@').map_or(helo_domain, |(_, d)| d),
589            helo_domain,
590            host_domain,
591            sender: Sender::MailFrom(sender),
592        }
593    }
594
595    /// Checks the HELO identity and then, unless it fails, the MAIL FROM
596    /// identity.
597    ///
598    /// Only [`MessageAuthenticator::verify_spf`] runs both checks; given to
599    /// [`MessageAuthenticator::check_host`], these parameters check the MAIL
600    /// FROM identity alone. Arguments are those of [`SpfParameters::mail_from`].
601    pub fn helo_and_mail_from(
602        ip: IpAddr,
603        helo_domain: &'x str,
604        host_domain: &'x str,
605        sender: &'x str,
606    ) -> SpfParameters<'x> {
607        SpfParameters {
608            ip,
609            domain: sender.rsplit_once('@').map_or(helo_domain, |(_, d)| d),
610            helo_domain,
611            host_domain,
612            sender: Sender::Full(sender),
613        }
614    }
615
616    /// Parameters for [`MessageAuthenticator::check_host`].
617    ///
618    /// - `ip`: the SMTP client IP address (`<ip>`).
619    /// - `domain`: the domain whose SPF record is evaluated (`<domain>`).
620    /// - `helo_domain`: the domain given in the HELO or EHLO command.
621    /// - `host_domain`: the name of the host performing the check (the `r` macro).
622    /// - `sender`: the MAIL FROM address (`<sender>`); an empty string is
623    ///   replaced by `postmaster@<domain>`.
624    ///
625    /// Given to [`MessageAuthenticator::verify_spf`], these parameters behave
626    /// like [`SpfParameters::helo_and_mail_from`] and `domain` is ignored.
627    pub fn new(
628        ip: IpAddr,
629        domain: &'x str,
630        helo_domain: &'x str,
631        host_domain: &'x str,
632        sender: &'x str,
633    ) -> Self {
634        SpfParameters {
635            ip,
636            domain,
637            helo_domain,
638            host_domain,
639            sender: Sender::Full(sender),
640        }
641    }
642
643    pub(crate) fn ip(&self) -> IpAddr {
644        self.ip
645    }
646
647    pub(crate) fn helo_domain(&self) -> &'x str {
648        self.helo_domain
649    }
650
651    pub(crate) fn host_domain(&self) -> &'x str {
652        self.host_domain
653    }
654
655    pub(crate) fn mail_from_address(&self) -> Option<&'x str> {
656        match self.sender {
657            Sender::Helo => None,
658            Sender::MailFrom(sender) | Sender::Full(sender) => Some(sender),
659        }
660    }
661}
662
663impl<'x> From<SpfParameters<'x>> for Parameters<'x, SpfParameters<'x>, NoCache> {
664    fn from(params: SpfParameters<'x>) -> Self {
665        Parameters::new(params)
666    }
667}
668
669trait IpMask {
670    fn matches_ipv4_mask(&self, addr: &Ipv4Addr, mask: u32) -> bool;
671    fn matches_ipv6_mask(&self, addr: &Ipv6Addr, mask: u128) -> bool;
672}
673
674impl IpMask for IpAddr {
675    fn matches_ipv4_mask(&self, addr: &Ipv4Addr, mask: u32) -> bool {
676        u32::from_be_bytes(match &self {
677            IpAddr::V4(ip) => ip.octets(),
678            IpAddr::V6(ip) => {
679                if let Some(ip) = ip.to_ipv4_mapped() {
680                    ip.octets()
681                } else {
682                    return false;
683                }
684            }
685        }) & mask
686            == u32::from_be_bytes(addr.octets()) & mask
687    }
688
689    fn matches_ipv6_mask(&self, addr: &Ipv6Addr, mask: u128) -> bool {
690        u128::from_be_bytes(match &self {
691            IpAddr::V6(ip) => ip.octets(),
692            IpAddr::V4(ip) => ip.to_ipv6_mapped().octets(),
693        }) & mask
694            == u128::from_be_bytes(addr.octets()) & mask
695    }
696}
697
698impl IpMask for Ipv6Addr {
699    fn matches_ipv6_mask(&self, addr: &Ipv6Addr, mask: u128) -> bool {
700        u128::from_be_bytes(self.octets()) & mask == u128::from_be_bytes(addr.octets()) & mask
701    }
702
703    fn matches_ipv4_mask(&self, _addr: &Ipv4Addr, _mask: u32) -> bool {
704        unimplemented!()
705    }
706}
707
708impl IpMask for Ipv4Addr {
709    fn matches_ipv4_mask(&self, addr: &Ipv4Addr, mask: u32) -> bool {
710        u32::from_be_bytes(self.octets()) & mask == u32::from_be_bytes(addr.octets()) & mask
711    }
712
713    fn matches_ipv6_mask(&self, _addr: &Ipv6Addr, _mask: u128) -> bool {
714        unimplemented!()
715    }
716}
717
718impl From<&Qualifier> for SpfResult {
719    fn from(q: &Qualifier) -> Self {
720        match q {
721            Qualifier::Pass => SpfResult::Pass,
722            Qualifier::Fail => SpfResult::Fail,
723            Qualifier::SoftFail => SpfResult::SoftFail,
724            Qualifier::Neutral => SpfResult::Neutral,
725        }
726    }
727}
728
729impl From<Error> for SpfResult {
730    fn from(err: Error) -> Self {
731        match err {
732            Error::Dns(DnsError::RecordNotFound(_)) | Error::Dns(DnsError::InvalidRecordType) => {
733                SpfResult::None
734            }
735            Error::Parse => SpfResult::PermError,
736            _ => SpfResult::TempError,
737        }
738    }
739}
740
741struct LookupLimit {
742    num_lookups: u32,
743    timer: Instant,
744}
745
746impl LookupLimit {
747    pub fn new() -> Self {
748        LookupLimit {
749            num_lookups: 1,
750            timer: Instant::now(),
751        }
752    }
753
754    #[inline(always)]
755    fn can_lookup(&mut self) -> bool {
756        if self.num_lookups <= 10 && self.timer.elapsed().as_secs() < 20 {
757            self.num_lookups += 1;
758            true
759        } else {
760            false
761        }
762    }
763}
764
765#[cfg(test)]
766#[allow(unused)]
767mod test {
768
769    use std::{
770        fs,
771        net::{IpAddr, Ipv4Addr, Ipv6Addr},
772        path::PathBuf,
773        time::{Duration, Instant},
774    };
775
776    use crate::{
777        MessageAuthenticator, Mx, SpfResult,
778        dns::cache::test::DummyCaches,
779        parse::TxtRecordParser,
780        spf::{Macro, SpfRecord},
781    };
782
783    use super::SpfParameters;
784
785    #[tokio::test]
786    async fn spf_verify() {
787        let resolver = MessageAuthenticator::new_system_conf().unwrap();
788        let valid_until = Instant::now() + Duration::from_secs(30);
789        let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
790        test_dir.push("resources");
791        test_dir.push("spf");
792
793        for file_name in fs::read_dir(&test_dir).unwrap() {
794            let file_name = file_name.unwrap().path();
795            println!("===== {} =====", file_name.display());
796            let test_suite = String::from_utf8(fs::read(&file_name).unwrap()).unwrap();
797            let caches = DummyCaches::new();
798
799            for test in test_suite.split("---\n") {
800                let mut test_name = "";
801                let mut last_test_name = "";
802                let mut helo = "";
803                let mut mail_from = "";
804                let mut client_ip = "127.0.0.1".parse::<IpAddr>().unwrap();
805                let mut test_num = 1;
806
807                for line in test.split('\n') {
808                    let line = line.trim();
809                    let line = if let Some(line) = line.strip_prefix('-') {
810                        line.trim()
811                    } else {
812                        line
813                    };
814
815                    if let Some(name) = line.strip_prefix("name:") {
816                        test_name = name.trim();
817                    } else if let Some(record) = line.strip_prefix("spf:") {
818                        let (name, record) = record.trim().split_once(' ').unwrap();
819                        caches.txt_add(
820                            name.trim().to_string(),
821                            SpfRecord::parse(record.as_bytes()),
822                            valid_until,
823                        );
824                    } else if let Some(record) = line.strip_prefix("exp:") {
825                        let (name, record) = record.trim().split_once(' ').unwrap();
826                        caches.txt_add(
827                            name.trim().to_string(),
828                            Macro::parse(record.as_bytes()),
829                            valid_until,
830                        );
831                    } else if let Some(record) = line.strip_prefix("a:") {
832                        let (name, record) = record.trim().split_once(' ').unwrap();
833                        caches.ipv4_add(
834                            name.trim().to_string(),
835                            record
836                                .split(',')
837                                .map(|item| item.trim().parse::<Ipv4Addr>().unwrap())
838                                .collect(),
839                            valid_until,
840                        );
841                    } else if let Some(record) = line.strip_prefix("aaaa:") {
842                        let (name, record) = record.trim().split_once(' ').unwrap();
843                        caches.ipv6_add(
844                            name.trim().to_string(),
845                            record
846                                .split(',')
847                                .map(|item| item.trim().parse::<Ipv6Addr>().unwrap())
848                                .collect(),
849                            valid_until,
850                        );
851                    } else if let Some(record) = line.strip_prefix("ptr:") {
852                        let (name, record) = record.trim().split_once(' ').unwrap();
853                        caches.ptr_add(
854                            name.trim().parse::<IpAddr>().unwrap(),
855                            record
856                                .split(',')
857                                .map(|item| Box::from(item.trim()))
858                                .collect(),
859                            valid_until,
860                        );
861                    } else if let Some(record) = line.strip_prefix("mx:") {
862                        let (name, record) = record.trim().split_once(' ').unwrap();
863                        let mut mxs = Vec::new();
864                        for (pos, item) in record.split(',').enumerate() {
865                            let ip = item.trim().parse::<IpAddr>().unwrap();
866                            let mx_name = format!("mx.{ip}.{pos}");
867                            match ip {
868                                IpAddr::V4(ip) => {
869                                    caches.ipv4_add(mx_name.clone(), vec![ip], valid_until)
870                                }
871                                IpAddr::V6(ip) => {
872                                    caches.ipv6_add(mx_name.clone(), vec![ip], valid_until)
873                                }
874                            }
875                            mxs.push(Mx {
876                                exchanges: Box::new([mx_name.into_boxed_str()]),
877                                preference: (pos + 1) as u16,
878                            });
879                        }
880                        caches.mx_add(name.trim().to_string(), mxs, valid_until);
881                    } else if let Some(value) = line.strip_prefix("domain:") {
882                        helo = value.trim();
883                    } else if let Some(value) = line.strip_prefix("sender:") {
884                        mail_from = value.trim();
885                    } else if let Some(value) = line.strip_prefix("ip:") {
886                        client_ip = value.trim().parse().unwrap();
887                    } else if let Some(value) = line.strip_prefix("expect:") {
888                        let value = value.trim();
889                        let (result, exp): (SpfResult, &str) =
890                            if let Some((result, exp)) = value.split_once(' ') {
891                                (result.trim().parse::<SpfResult>().unwrap(), exp.trim())
892                            } else {
893                                (value.parse::<SpfResult>().unwrap(), "")
894                            };
895                        let output = resolver
896                            .verify_spf(caches.parameters(SpfParameters::helo_and_mail_from(
897                                client_ip,
898                                helo,
899                                "localdomain.org",
900                                mail_from,
901                            )))
902                            .await;
903                        assert_eq!(
904                            output.result(),
905                            result,
906                            "Failed for {test_name:?}, test {test_num}, ehlo: {helo}, mail-from: {mail_from}.",
907                        );
908
909                        if !exp.is_empty() {
910                            assert_eq!(Some(exp.to_string()).as_deref(), output.explanation());
911                        }
912                        test_num += 1;
913                        if test_name != last_test_name {
914                            println!("Passed test {test_name:?}");
915                            last_test_name = test_name;
916                        }
917                    }
918                }
919            }
920        }
921    }
922}