spamassassin_milter/
config.rs1use ipnet::IpNet;
18use std::{collections::HashSet, net::IpAddr};
19
20#[derive(Clone, Debug, Eq, PartialEq)]
22pub struct ConfigBuilder {
23 use_trusted_networks: bool,
24 trusted_networks: HashSet<IpNet>,
25 auth_untrusted: bool,
26 spamc_args: Vec<String>,
27 max_message_size: usize,
28 dry_run: bool,
29 reject_spam: bool,
30 reply_code: String,
31 reply_status_code: String,
32 reply_text: String,
33 preserve_headers: bool,
34 preserve_body: bool,
35 verbose: bool,
36}
37
38impl ConfigBuilder {
39 pub fn use_trusted_networks(mut self, value: bool) -> Self {
40 self.use_trusted_networks = value;
41 self
42 }
43
44 pub fn trusted_network(mut self, net: IpNet) -> Self {
45 self.use_trusted_networks = true;
46 self.trusted_networks.insert(net);
47 self
48 }
49
50 pub fn auth_untrusted(mut self, value: bool) -> Self {
51 self.auth_untrusted = value;
52 self
53 }
54
55 pub fn spamc_args<I, S>(mut self, args: I) -> Self
56 where
57 I: IntoIterator<Item = S>,
58 S: AsRef<str>,
59 {
60 self.spamc_args.extend(args.into_iter().map(|a| a.as_ref().to_owned()));
61 self
62 }
63
64 pub fn max_message_size(mut self, value: usize) -> Self {
65 self.max_message_size = value;
66 self
67 }
68
69 pub fn dry_run(mut self, value: bool) -> Self {
70 self.dry_run = value;
71 self
72 }
73
74 pub fn reject_spam(mut self, value: bool) -> Self {
75 self.reject_spam = value;
76 self
77 }
78
79 pub fn reply_code(mut self, value: String) -> Self {
80 self.reply_code = value;
81 self
82 }
83
84 pub fn reply_status_code(mut self, value: String) -> Self {
85 self.reply_status_code = value;
86 self
87 }
88
89 pub fn reply_text(mut self, value: String) -> Self {
90 self.reply_text = value;
91 self
92 }
93
94 pub fn preserve_headers(mut self, value: bool) -> Self {
95 self.preserve_headers = value;
96 self
97 }
98
99 pub fn preserve_body(mut self, value: bool) -> Self {
100 self.preserve_body = value;
101 self
102 }
103
104 pub fn verbose(mut self, value: bool) -> Self {
105 self.verbose = value;
106 self
107 }
108
109 pub fn build(self) -> Config {
110 assert!(
113 self.use_trusted_networks || self.trusted_networks.is_empty(),
114 "trusted networks present but not used"
115 );
116 assert!(
117 is_valid_reply_code(&self.reply_code)
118 && is_valid_reply_code(&self.reply_status_code)
119 && self.reply_code.as_bytes()[0] == self.reply_status_code.as_bytes()[0],
120 "invalid or incompatible reply codes"
121 );
122
123 Config {
124 use_trusted_networks: self.use_trusted_networks,
125 trusted_networks: self.trusted_networks,
126 auth_untrusted: self.auth_untrusted,
127 spamc_args: self.spamc_args,
128 max_message_size: self.max_message_size,
129 dry_run: self.dry_run,
130 reject_spam: self.reject_spam,
131 reply_code: self.reply_code,
132 reply_status_code: self.reply_status_code,
133 reply_text: self.reply_text,
134 preserve_headers: self.preserve_headers,
135 preserve_body: self.preserve_body,
136 verbose: self.verbose,
137 }
138 }
139}
140
141fn is_valid_reply_code(s: &str) -> bool {
142 s.starts_with('4') || s.starts_with('5')
143}
144
145impl Default for ConfigBuilder {
146 fn default() -> Self {
147 Self {
148 use_trusted_networks: Default::default(),
149 trusted_networks: Default::default(),
150 auth_untrusted: Default::default(),
151 spamc_args: Default::default(),
152 max_message_size: 512_000, dry_run: Default::default(),
154 reject_spam: Default::default(),
155 reply_code: "550".into(),
158 reply_status_code: "5.7.1".into(),
159 reply_text: "Spam message refused".into(),
161 preserve_headers: Default::default(),
162 preserve_body: Default::default(),
163 verbose: Default::default(),
164 }
165 }
166}
167
168#[derive(Clone, Debug, Eq, PartialEq)]
170pub struct Config {
171 use_trusted_networks: bool,
172 trusted_networks: HashSet<IpNet>,
173 auth_untrusted: bool,
174 spamc_args: Vec<String>,
175 max_message_size: usize,
176 dry_run: bool,
177 reject_spam: bool,
178 reply_code: String,
179 reply_status_code: String,
180 reply_text: String,
181 preserve_headers: bool,
182 preserve_body: bool,
183 verbose: bool,
184}
185
186impl Config {
187 pub fn builder() -> ConfigBuilder {
188 Default::default()
189 }
190
191 pub fn use_trusted_networks(&self) -> bool {
192 self.use_trusted_networks
193 }
194
195 pub fn is_in_trusted_networks(&self, ip: &IpAddr) -> bool {
196 self.trusted_networks.iter().any(|n| n.contains(ip))
197 }
198
199 pub fn auth_untrusted(&self) -> bool {
200 self.auth_untrusted
201 }
202
203 pub fn spamc_args(&self) -> &[String] {
204 &self.spamc_args
205 }
206
207 pub fn max_message_size(&self) -> usize {
208 self.max_message_size
209 }
210
211 pub fn dry_run(&self) -> bool {
212 self.dry_run
213 }
214
215 pub fn reject_spam(&self) -> bool {
216 self.reject_spam
217 }
218
219 pub fn reply_code(&self) -> &str {
220 &self.reply_code
221 }
222
223 pub fn reply_status_code(&self) -> &str {
224 &self.reply_status_code
225 }
226
227 pub fn reply_text(&self) -> &str {
228 &self.reply_text
229 }
230
231 pub fn preserve_headers(&self) -> bool {
232 self.preserve_headers
233 }
234
235 pub fn preserve_body(&self) -> bool {
236 self.preserve_body
237 }
238
239 pub fn verbose(&self) -> bool {
240 self.verbose
241 }
242}
243
244impl Default for Config {
245 fn default() -> Self {
246 ConfigBuilder::default().build()
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn trusted_networks_config() {
256 let config = Config::builder()
257 .trusted_network("127.0.0.1/8".parse().unwrap())
258 .build();
259
260 assert!(config.use_trusted_networks());
261 assert!(config.is_in_trusted_networks(&"127.0.0.1".parse().unwrap()));
262 assert!(!config.is_in_trusted_networks(&"10.1.0.1".parse().unwrap()));
263 }
264
265 #[test]
266 fn spamc_args_extends_args() {
267 let config = Config::builder()
268 .spamc_args(["-p", "3030"])
269 .spamc_args(["-x"])
270 .build();
271
272 assert_eq!(
273 config.spamc_args(),
274 &[String::from("-p"), String::from("3030"), String::from("-x")],
275 );
276 }
277}