decon_spf/spf/
string.rs

1use crate::mechanism::{Kind, Mechanism};
2use crate::spf::errors::SpfErrors;
3use crate::spf::validate::{self, check_whitespaces, Validate};
4use crate::{Spf, SpfError};
5use ipnetwork::IpNetwork;
6use std::convert::TryFrom;
7use std::fmt::{Display, Formatter};
8use std::str::FromStr;
9
10impl Display for Spf<String> {
11    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
12        if !&self.source.is_empty() {
13            write!(f, "{}", self.source)
14        } else {
15            let mut spf_string = String::new();
16            spf_string.push_str(self.version().as_str());
17            for m in self.iter() {
18                spf_string.push_str(format!(" {}", m).as_str());
19            }
20            write!(f, "{}", spf_string)
21        }
22    }
23}
24
25/// Implement parse for `Spf<String>`
26/// # Errors
27/// - Invalid Version
28/// - String length exceeds 512 octets (characters)
29///
30/// # Soft Errors
31/// These will be found when calling `validate()` on `Spf<String>`
32impl FromStr for Spf<String> {
33    type Err = SpfError;
34    fn from_str(s: &str) -> Result<Self, Self::Err> {
35        validate::check_start_of_spf(s)?;
36        validate::check_spf_length(s)?;
37
38        // Index of Redirect Mechanism
39        let mut redirect_idx: usize = 0;
40        // Index of All Mechanism
41        let mut all_idx = 0;
42        let mut idx = 0;
43        let mut spf = Spf::default();
44        let mechanisms = s.split_whitespace();
45        for m in mechanisms {
46            if m.contains(crate::core::SPF1) {
47                spf.version = m.to_string();
48            } else if m.contains(crate::core::IP4) || m.contains(crate::core::IP6) {
49                let m_ip = m.parse::<Mechanism<IpNetwork>>()?;
50                spf.mechanisms.push(m_ip.into());
51            } else {
52                let m_str = m.parse::<Mechanism<String>>()?;
53                spf.lookup_count += Self::update_lookup_count(&m_str);
54                match *m_str.kind() {
55                    Kind::Redirect => {
56                        if !spf.has_redirect {
57                            spf.has_redirect = true;
58                            redirect_idx = idx;
59                        } else {
60                            return Err(SpfError::ModifierMayOccurOnlyOnce(Kind::Redirect));
61                        }
62                    }
63                    Kind::All => {
64                        all_idx = idx;
65                    }
66                    _ => {}
67                }
68                spf.mechanisms.push(m_str);
69                idx += 1;
70            }
71        }
72        spf.source = s.to_string();
73        spf.redirect_idx = redirect_idx;
74        spf.all_idx = all_idx;
75        Ok(spf)
76    }
77}
78
79impl TryFrom<&str> for Spf<String> {
80    type Error = SpfError;
81
82    fn try_from(s: &str) -> Result<Self, Self::Error> {
83        Spf::from_str(s)
84    }
85}
86
87impl Spf<String> {
88    /// Creates a `Spf<String>` from the passed str reference.
89    /// This is basically a rapper around FromStr which has been implemented for `Spf<String>`
90    #[allow(dead_code)]
91    pub fn new(s: &str) -> Result<Self, SpfError> {
92        s.parse::<Spf<String>>()
93    }
94
95    /// Check that version is v1
96    pub fn is_v1(&self) -> bool {
97        self.version.contains(crate::core::SPF1)
98    }
99    /// Check if the Spf record was created from [`crate::SpfBuilder<Builder>`] or from `&str`
100    /// ```
101    /// # use decon_spf::Spf;
102    /// # use decon_spf::mechanism::{Mechanism, MechanismError, Qualifier};
103    /// let spf = "v=spf1 -all".parse::<Spf<String>>().unwrap();
104    /// assert!(!spf.built());
105    /// ```
106    pub fn built(&self) -> bool {
107        self.source.is_empty()
108    }
109    /// Give access to the redirect modifier if present
110    pub fn redirect(&self) -> Option<&Mechanism<String>> {
111        if self.redirect_idx == 0 {
112            match self
113                .mechanisms
114                .first()
115                .expect("There should be a Mechanism<>")
116                .kind()
117            {
118                Kind::Redirect => self.mechanisms.first(),
119                _ => None,
120            }
121        } else {
122            Some(&self.mechanisms[self.redirect_idx])
123        }
124    }
125    /// Give access to the `all` mechanism if it is present.
126    pub fn all(&self) -> Option<&Mechanism<String>> {
127        if self.all_idx == 0 {
128            match self
129                .mechanisms
130                .first()
131                .expect("There should be a Mechanism<>")
132                .kind()
133            {
134                Kind::All => self.mechanisms.first(),
135                _ => None,
136            }
137        } else {
138            Some(&self.mechanisms[self.all_idx])
139        }
140    }
141
142    /// Validation for `Spf<String>`
143    /// # Examples
144    /// ```rust
145    /// use decon_spf::{Spf, SpfError, SpfErrors};
146    /// let spf = "v=spf1 -all".parse::<Spf<String>>().unwrap();
147    /// assert!(spf.validate().is_ok());
148    /// let spf = "v=spf1 redirect=_spf.example.com -all".parse::<Spf<String>>().unwrap();
149    /// assert!(spf.validate().is_err());
150    /// let spf: SpfErrors = spf.validate().unwrap_err();
151    /// println!("{}", spf.source());
152    /// for e in spf.errors() {
153    ///     # assert_eq!(e.to_string(), "Spf record contains both a 'REDIRECT' modifier and 'ALL' mechanism.\nAccording to RFC7208 any redirect MUST be ignored in this case.\n[See Section 5.1](https://datatracker.ietf.org/doc/html/rfc7208#section-5.1)");
154    ///     println!("{}",e);
155    /// }
156    /// ```
157    /// # Returns
158    /// Either Ok or a [SpfErrors] containing any [SpfError]
159    /// # Errors:
160    /// - Hard Errors
161    ///     - [Invalid version](SpfError::InvalidVersion)
162    ///     - [Source Length Exceeded](SpfError::SourceLengthExceeded)
163    /// - Soft Errors
164    ///     - [Deprecated PTR detected](SpfError::DeprecatedPtrDetected)
165    ///     - [Lookup Count Exceeded](SpfError::LookupLimitExceeded)
166    ///     - [Redirect & All](SpfError::RedirectWithAllMechanism)
167    ///     - [Redirect Position](SpfError::RedirectNotFinalMechanism)
168    pub fn validate(&self) -> Result<(), SpfErrors> {
169        let mut errors = SpfErrors::new();
170
171        // Handle hard errors that stop further validation
172        for check in [self.validate_version(), self.validate_length()] {
173            if let Err(e) = check {
174                errors.register_source(self.source.clone());
175                errors.register_error(e);
176                return Err(errors);
177            }
178        }
179
180        // Handle soft errors that allow continued validation
181        let soft_checks = [
182            self.validate_ptr(),
183            self.validate_lookup_count(),
184            self.validate_redirect_all(),
185            // todo: Consider changing this to be part of Trait Validate??
186            check_whitespaces(&self.source),
187        ];
188
189        for check in soft_checks {
190            if let Err(e) = check {
191                errors.register_error(e);
192            }
193        }
194        // Return errors if any occurred
195        if errors.errors().is_empty() {
196            Ok(())
197        } else {
198            errors.register_source(self.source.clone());
199            Err(errors)
200        }
201    }
202
203    // If the Mechanism will cause a DNS Lookup 1 should be added to the `lookup_count`. Otherwise 0
204    fn update_lookup_count(m_str: &Mechanism<String>) -> u8 {
205        match *m_str.kind() {
206            Kind::Redirect | Kind::A | Kind::MX | Kind::Include | Kind::Ptr | Kind::Exists => 1,
207            _ => 0,
208        }
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use crate::Spf;
215
216    #[cfg(feature = "ptr")]
217    use crate::SpfError;
218    #[test]
219    fn basic_disallow() {
220        let spf = "v=spf1 -all".parse::<Spf<String>>().unwrap();
221        assert!(!spf.source.is_empty());
222        assert_eq!(spf.redirect(), None);
223        assert_eq!(spf.has_redirect, false);
224        assert_eq!(spf.all_idx, 0);
225        assert_eq!(spf.lookup_count(), 0);
226        assert_eq!(spf.all().unwrap().to_string(), "-all");
227        let validation_result = spf.validate();
228        assert!(validation_result.is_ok());
229    }
230    #[test]
231    #[cfg(not(feature = "ptr"))]
232    fn ptr_allowed_() {
233        let spf = "v=spf1 ptr -all".parse::<Spf<String>>().unwrap();
234        assert!(!spf.source.is_empty());
235        assert_eq!(spf.redirect(), None);
236        assert_eq!(spf.has_redirect, false);
237        assert_eq!(spf.all_idx, 1);
238        let validation_result = spf.validate();
239        assert!(validation_result.is_ok());
240    }
241    #[test]
242    #[cfg(feature = "ptr")]
243    fn ptr_not_allowed_() {
244        let spf = "v=spf1 ptr -all".parse::<Spf<String>>().unwrap();
245        assert!(!spf.source.is_empty());
246        assert_eq!(spf.redirect(), None);
247        assert_eq!(spf.has_redirect, false);
248        assert_eq!(spf.all_idx, 1);
249        let validation_result_vec = spf.validate();
250        assert!(validation_result_vec.is_err());
251        let result = validation_result_vec.unwrap_err();
252        assert_eq!(result.errors()[0], SpfError::DeprecatedPtrDetected);
253    }
254
255    mod hard_errors {
256        use crate::mechanism::Kind;
257        use crate::{Spf, SpfError};
258
259        #[test]
260        fn multiple_redirects() {
261            let spf = "v=spf1 redirect=_spf.example.com redirect=_spf.example.com"
262                .parse::<Spf<String>>()
263                .unwrap_err();
264            assert_eq!(spf, SpfError::ModifierMayOccurOnlyOnce(Kind::Redirect));
265        }
266    }
267    mod soft_errors {
268        use crate::{Spf, SpfError};
269
270        #[test]
271        fn redirect_with_all() {
272            let spf = "v=spf1 redirect=_spf.example.com -all"
273                .parse::<Spf<String>>()
274                .unwrap()
275                .validate();
276
277            assert_eq!(
278                spf.unwrap_err().errors()[0],
279                SpfError::RedirectWithAllMechanism
280            );
281        }
282        #[test]
283        fn all_with_redirect() {
284            let spf = "v=spf1 -all redirect=_spf.example.com"
285                .parse::<Spf<String>>()
286                .unwrap()
287                .validate();
288            assert_eq!(
289                spf.unwrap_err().errors()[0],
290                SpfError::RedirectWithAllMechanism
291            );
292        }
293
294        #[cfg(feature = "strict-dns")]
295        mod strict_dns {
296            use crate::mechanism::MechanismError;
297            use crate::{Spf, SpfError};
298
299            #[test]
300            fn test() {
301                let spf = "v=spf1 redirect=_spf.example.xx -all"
302                    .parse::<Spf<String>>()
303                    .unwrap_err();
304                assert!(matches!(
305                    spf,
306                    SpfError::InvalidMechanism(MechanismError::InvalidDomainHost(_))
307                ))
308            }
309        }
310    }
311}