1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
use crate::record::ExplainString;
use std::{
    fmt::{self, Debug, Formatter},
    time::Duration,
};

/// A builder for SPF query configurations.
pub struct ConfigBuilder {
    max_void_lookups: usize,
    timeout: Duration,
    default_explanation: Option<ExplainString>,
    modify_exp_fn: Option<Box<dyn Fn(&mut ExplainString) + Send + Sync>>,
    hostname: Option<String>,
    capture_trace: bool,
}

impl ConfigBuilder {
    /// The maximum number of void lookups allowed. The default is 2.
    pub fn max_void_lookups(mut self, value: usize) -> Self {
        self.max_void_lookups = value;
        self
    }

    /// The duration until a query times out. The default is 20 seconds.
    ///
    /// This is not an accurate, enforced timeout. Rather, the duration is the
    /// minimum amount of time until the library may detect a timeout. If a
    /// `Lookup` implementation itself uses a much longer timeout duration, then
    /// the timeout condition will not be detected until after that timeout has
    /// passed. The time elapsed is checked on every attempt to perform DNS
    /// resolution.
    pub fn timeout(mut self, value: Duration) -> Self {
        self.timeout = value;
        self
    }

    /// The explain string from which to compute the default explanation when a
    /// `Fail` result is returned. The default value is the empty string.
    ///
    /// When the given `ExplainString` is evaluated, it is expanded strictly
    /// locally, without access to the network. Specifically, the `%{p}` macro
    /// therefore always expands to the default value `unknown`.
    pub fn default_explanation<S>(mut self, value: S) -> Self
    where
        S: Into<ExplainString>,
    {
        self.default_explanation = Some(value.into());
        self
    }

    /// The function to apply to explain strings originating from the DNS via an
    /// *exp* modifier.
    ///
    /// This function serves as a callback that allows sanitising or otherwise
    /// altering the third-party-provided explain string before it undergoes
    /// macro expansion. An example use would be prepending a macro string such
    /// as `"%{o} explains: "`.
    pub fn modify_exp_with<F>(mut self, f: F) -> Self
    where
        F: Fn(&mut ExplainString) + Send + Sync + 'static,
    {
        self.modify_exp_fn = Some(Box::new(f));
        self
    }

    /// The hostname of the SPF verifier.
    ///
    /// This is substituted for the `%{r}` macro during macro expansion. The
    /// given string is used as-is, without validation or transformation.
    pub fn hostname<S>(mut self, value: S) -> Self
    where
        S: Into<String>,
    {
        self.hostname = Some(value.into());
        self
    }

    /// Whether to capture a trace during query execution.
    ///
    /// Note: Enabling tracing has a cost, as the data structures encountered
    /// during processing are copied and aggregated in the trace.
    pub fn capture_trace(mut self, value: bool) -> Self {
        self.capture_trace = value;
        self
    }

    /// Constructs a new configuration.
    pub fn build(self) -> Config {
        Config {
            max_void_lookups: self.max_void_lookups,
            timeout: self.timeout,
            default_explanation: self.default_explanation,
            modify_exp_fn: self.modify_exp_fn,
            hostname: self.hostname,
            capture_trace: self.capture_trace,
        }
    }
}

impl Default for ConfigBuilder {
    fn default() -> Self {
        Self {
            // §4.6.4: ‘SPF implementations SHOULD limit "void lookups" to two.
            // An implementation MAY choose to make such a limit configurable.
            // In this case, a default of two is RECOMMENDED.’
            max_void_lookups: 2,
            // ‘MTAs or other processors SHOULD impose a limit on the maximum
            // amount of elapsed time to evaluate check_host(). Such a limit
            // SHOULD allow at least 20 seconds.’
            timeout: Duration::from_secs(20),
            default_explanation: None,
            modify_exp_fn: None,
            hostname: None,
            capture_trace: false,
        }
    }
}

/// An SPF query configuration.
pub struct Config {
    // Note: Keep fields in sync with the `Debug` implementation below.
    max_void_lookups: usize,
    timeout: Duration,
    default_explanation: Option<ExplainString>,
    modify_exp_fn: Option<Box<dyn Fn(&mut ExplainString) + Send + Sync>>,
    hostname: Option<String>,
    capture_trace: bool,
}

impl Config {
    /// Creates a builder for configurations.
    pub fn builder() -> ConfigBuilder {
        Default::default()
    }

    /// The maximum number of void lookups allowed.
    pub fn max_void_lookups(&self) -> usize {
        self.max_void_lookups
    }

    /// The duration until a query times out.
    pub fn timeout(&self) -> Duration {
        self.timeout
    }

    /// The explain string from which to compute the default explanation when a
    /// `Fail` result is returned.
    pub fn default_explanation(&self) -> Option<&ExplainString> {
        self.default_explanation.as_ref()
    }

    /// The function to apply to explain strings originating from the DNS via an
    /// *exp* modifier.
    pub fn modify_exp_fn(&self) -> Option<&(impl Fn(&mut ExplainString) + Send + Sync)> {
        self.modify_exp_fn.as_ref()
    }

    /// The hostname of the SPF verifier.
    pub fn hostname(&self) -> Option<&str> {
        self.hostname.as_deref()
    }

    /// Whether to capture a trace during query execution.
    pub fn capture_trace(&self) -> bool {
        self.capture_trace
    }
}

impl Default for Config {
    fn default() -> Self {
        ConfigBuilder::default().build()
    }
}

// A manual `Debug` implementation is needed to accomodate the `modify_exp_fn`
// closure, but should otherwise be exactly like a derived one.
impl Debug for Config {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.debug_struct("Config")
            .field("max_void_lookups", &self.max_void_lookups)
            .field("timeout", &self.timeout)
            .field("default_explanation", &self.default_explanation)
            .field("modify_exp_fn", &"<closure>")
            .field("hostname", &self.hostname)
            .field("capture_trace", &self.capture_trace)
            .finish()
    }
}

impl From<ConfigBuilder> for Config {
    fn from(builder: ConfigBuilder) -> Self {
        builder.build()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn default_void_lookups_ok() {
        assert_eq!(Config::default().max_void_lookups(), 2);
    }

    #[test]
    fn modify_exp_with_ok() {
        let prefix = String::from("%{o} explains: ");
        let config = Config::builder()
            .modify_exp_with(move |explain_string| {
                let prefix = prefix.parse::<ExplainString>().unwrap();
                explain_string.segments.splice(..0, prefix);
            })
            .build();

        let f = config.modify_exp_fn().unwrap();

        let mut explain_string = "not authorized".parse().unwrap();
        f(&mut explain_string);

        assert_eq!(explain_string, "%{o} explains: not authorized".parse().unwrap());
    }
}