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
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
// viaspf – implementation of the SPF specification
// Copyright © 2020–2023 David Bürgin <dbuergin@gluet.ch>
//
// This program is free software: you can redistribute it and/or modify it under
// the terms of the GNU General Public License as published by the Free Software
// Foundation, either version 3 of the License, or (at your option) any later
// version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
// details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see https://www.gnu.org/licenses/.

use crate::record::ExplainString;
use std::{
    fmt::{self, Debug, Formatter},
    time::Duration,
};

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

impl ConfigBuilder {
    /// The maximum number of DNS lookups allowed. The default is 10.
    ///
    /// This setting is used separately for both the global, per-query limit,
    /// and the per-mechanism limit for the *mx* and *ptr* mechanisms.
    ///
    /// This setting **must not** be changed from the default 10, if conformance
    /// with RFC 7208 is desired. It is provided for exceptional circumstances
    /// only.
    pub fn max_lookups(mut self, value: usize) -> Self {
        self.max_lookups = value;
        self
    }

    /// 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.
    ///
    /// A lookup implementation may contain its own timeout logic, to make sure
    /// each request doesn’t take too much time. This setting is independent
    /// from the lookup implementation, a query will simply be dropped when this
    /// timeout expires.
    ///
    /// Note: The timeout is only effective when the (default) feature
    /// `tokio-timeout` is enabled.
    pub fn timeout(mut self, value: Duration) -> Self {
        self.timeout = value;
        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_lookups: self.max_lookups,
            max_void_lookups: self.max_void_lookups,
            timeout: self.timeout,
            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: ‘The following terms cause DNS queries […]. SPF
            // implementations MUST limit the total number of those terms to 10
            // during SPF evaluation’. Two distinct lookup limits – global and
            // per-mechanism – both use this value and are not separately
            // configurable for now.
            max_lookups: 10,
            // ‘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),
            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_lookups: usize,
    max_void_lookups: usize,
    timeout: Duration,
    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 DNS lookups allowed.
    pub fn max_lookups(&self) -> usize {
        self.max_lookups
    }

    /// 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 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_lookups", &self.max_lookups)
            .field("max_void_lookups", &self.max_void_lookups)
            .field("timeout", &self.timeout)
            .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 = "%{o} explains: ".to_owned();
        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()
        );
    }
}