Author: Kevin Schoon [me@kevinschoon.com]
Hash: 63932a6d5e8a1125a77e50151497f9d4990ed8af
Timestamp: Wed, 09 Oct 2024 13:15:18 +0000 (1 week ago)

+151 -37 +/-6 browse
fix spf validation failure
1diff --git a/maitred-debug/src/config.rs b/maitred-debug/src/config.rs
2index d4c69b0..9a5ff27 100644
3--- a/maitred-debug/src/config.rs
4+++ b/maitred-debug/src/config.rs
5 @@ -7,19 +7,18 @@ pub(crate) struct Account {
6
7 #[derive(Clone, serde::Deserialize)]
8 pub(crate) struct Spf {
9- pub enabled: bool
10+ pub enabled: bool,
11 }
12
13-
14 #[derive(Clone, serde::Deserialize)]
15 pub(crate) struct Dkim {
16- pub enabled: bool
17+ pub enabled: bool,
18 }
19
20 #[derive(Clone, serde::Deserialize)]
21 pub(crate) struct Tls {
22 pub certificate: PathBuf,
23- pub key: PathBuf
24+ pub key: PathBuf,
25 }
26
27 #[derive(serde::Deserialize)]
28 diff --git a/maitred.toml b/maitred.toml
29index f785cc7..0dac335 100644
30--- a/maitred.toml
31+++ b/maitred.toml
32 @@ -1,8 +1,11 @@
33 # Path of the directory to deliver mail in the "maildir" format to
34 maildir = "mail"
35
36+ # Hostname of our server
37+ hostname = "localhost:2525"
38+
39 # logging level
40- # level = "TRACE"
41+ level = "DEBUG"
42
43 # address to bind to
44 address = "0.0.0.0:2525"
45 @@ -18,8 +21,7 @@ key = "key.pem"
46 enabled = false
47
48 [spf]
49- enabled = false
50-
51+ enabled = true
52
53 [[accounts]]
54 address = "demo-1@example.org"
55 diff --git a/maitred/src/auth.rs b/maitred/src/auth.rs
56index 0494e65..4bc6eea 100644
57--- a/maitred/src/auth.rs
58+++ b/maitred/src/auth.rs
59 @@ -4,8 +4,8 @@ use async_trait::async_trait;
60 use base64::{prelude::*, DecodeError};
61 use stringprep::{saslprep, Error as SaslPrepError};
62
63- use crate::smtp_response;
64 use crate::session::Response;
65+ use crate::smtp_response;
66 use smtp_proto::Response as SmtpResponse;
67
68 /// Any error that occurred during authentication.
69 diff --git a/maitred/src/server.rs b/maitred/src/server.rs
70index 8653187..b9ef15d 100644
71--- a/maitred/src/server.rs
72+++ b/maitred/src/server.rs
73 @@ -72,6 +72,7 @@ pub(crate) enum Action {
74 /// as they are received.
75 pub struct Server {
76 address: String,
77+ our_hostname: String,
78 global_timeout: Duration,
79 pipelining: bool,
80 milter: Option<Arc<dyn Milter>>,
81 @@ -93,6 +94,7 @@ impl Default for Server {
82 fn default() -> Self {
83 Server {
84 address: DEFAULT_LISTEN_ADDR.to_string(),
85+ our_hostname: String::default(),
86 global_timeout: Duration::from_secs(DEFAULT_GLOBAL_TIMEOUT_SECS),
87 pipelining: true,
88 milter: None,
89 @@ -120,6 +122,12 @@ impl Server {
90 self
91 }
92
93+ /// The hostname of this server
94+ pub fn our_hostname(mut self, hostname: &str) -> Self {
95+ self.our_hostname = hostname.to_string();
96+ self
97+ }
98+
99 /// Set the maximum amount of time the server will wait for another command
100 /// before closing the connection. RFC states the suggested time is 5m.
101 pub fn timeout(mut self, timeout: Duration) -> Self {
102 @@ -284,6 +292,10 @@ impl Server {
103 conn.send(response).await?;
104 return Ok(Action::Continue);
105 }
106+ crate::session::Action::Quit(response) => {
107+ conn.send(response).await?;
108+ return Ok(Action::Shutdown);
109+ }
110 _ => unreachable!(),
111 }
112 }
113 @@ -383,8 +395,11 @@ impl Server {
114 .session
115 .clone()
116 .client_ip(remote_addr.ip())
117+ .our_hostname(&self.our_hostname)
118 .starttls(self.tls_certificates.is_some())
119- .expn_enabled(self.list_expansion.is_some());
120+ .vrfy_enabled(self.verification.is_some())
121+ .expn_enabled(self.list_expansion.is_some())
122+ .spf_verification(self.spf_verification);
123
124 let mut framed = Framed::new(
125 &mut *stream,
126 diff --git a/maitred/src/session.rs b/maitred/src/session.rs
127index da894ca..2b29fdf 100644
128--- a/maitred/src/session.rs
129+++ b/maitred/src/session.rs
130 @@ -555,7 +555,7 @@ impl Session {
131 let ip_addr = match self.client_ip {
132 Some(ip_addr) => ip_addr,
133 None => {
134- return Action::Send(smtp_response!(
135+ return Action::Quit(smtp_response!(
136 500,
137 0,
138 0,
139 @@ -567,7 +567,7 @@ impl Session {
140 let helo_domain = match &self.hostname {
141 Some(helo_domain) => helo_domain.to_string(),
142 None => {
143- return Action::Send(smtp_response!(
144+ return Action::Quit(smtp_response!(
145 500,
146 0,
147 0,
148 @@ -591,7 +591,7 @@ impl Session {
149 inner.spf_verified_host = Some(helo_domain.clone());
150 Action::Send(smtp_response!(250, 0, 0, 0, "OK"))
151 } else {
152- Action::Send(smtp_response!(
153+ Action::Quit(smtp_response!(
154 500,
155 0,
156 0,
157 @@ -856,6 +856,8 @@ impl Session {
158 #[cfg(test)]
159 mod test {
160
161+ use std::net::Ipv4Addr;
162+
163 use base64::engine::general_purpose::STANDARD;
164 use base64::prelude::*;
165 use smtp_proto::MailFrom;
166 @@ -953,6 +955,103 @@ mod test {
167 }
168
169 #[test]
170+ fn session_spf_successful() {
171+ let mut session = Session::default()
172+ .our_hostname("localhost:2525")
173+ .spf_verification(true)
174+ .client_ip(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
175+ assert!(equal(
176+ &session.next(Some(&Request::Helo {
177+ host: EXAMPLE_HOSTNAME.to_string(),
178+ })),
179+ &Action::Send(smtp_response!(
180+ 250,
181+ 0,
182+ 0,
183+ 0,
184+ String::from("Hello example.org")
185+ )),
186+ ));
187+ match session.next(Some(&Request::Mail {
188+ from: MailFrom {
189+ address: String::from("fuu@example.org"),
190+ ..Default::default()
191+ },
192+ })) {
193+ Action::SpfVerification {
194+ ip_addr,
195+ helo_domain,
196+ host_domain,
197+ mail_from,
198+ cb,
199+ } => {
200+ assert!(ip_addr.eq(&IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))));
201+ assert!(helo_domain.eq(EXAMPLE_HOSTNAME));
202+ assert!(host_domain.eq("localhost:2525"));
203+ assert!(mail_from.as_str().eq("fuu@example.org"));
204+ assert!(equal(
205+ &cb(true),
206+ &Action::Send(smtp_response!(250, 0, 0, 0, "OK"))
207+ ))
208+ }
209+ _ => {
210+ unreachable!();
211+ }
212+ }
213+
214+ assert!(session
215+ .hostname
216+ .as_ref()
217+ .is_some_and(|hostname| hostname.to_string() == EXAMPLE_HOSTNAME));
218+ }
219+
220+ #[test]
221+ fn session_spf_failed() {
222+ let mut session = Session::default()
223+ .our_hostname("localhost:2525")
224+ .spf_verification(true)
225+ .client_ip(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
226+ assert!(equal(
227+ &session.next(Some(&Request::Helo {
228+ host: EXAMPLE_HOSTNAME.to_string(),
229+ })),
230+ &Action::Send(smtp_response!(
231+ 250,
232+ 0,
233+ 0,
234+ 0,
235+ String::from("Hello example.org")
236+ )),
237+ ));
238+ match session.next(Some(&Request::Mail {
239+ from: MailFrom {
240+ address: String::from("fuu@example.org"),
241+ ..Default::default()
242+ },
243+ })) {
244+ Action::SpfVerification {
245+ ip_addr,
246+ helo_domain,
247+ host_domain,
248+ mail_from,
249+ cb,
250+ } => {
251+ assert!(ip_addr.eq(&IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))));
252+ assert!(helo_domain.eq(EXAMPLE_HOSTNAME));
253+ assert!(host_domain.eq("localhost:2525"));
254+ assert!(mail_from.as_str().eq("fuu@example.org"));
255+ assert!(equal(
256+ &cb(false),
257+ &Action::Quit(smtp_response!(500, 0, 0, 0, "SPF Verification Failed"))
258+ ))
259+ }
260+ _ => {
261+ unreachable!();
262+ }
263+ };
264+ }
265+
266+ #[test]
267 fn session_command_with_no_helo() {
268 let mut session = Session::default();
269 assert!(equal(
270 diff --git a/maitred/src/transport.rs b/maitred/src/transport.rs
271index 15e2430..ae847f6 100644
272--- a/maitred/src/transport.rs
273+++ b/maitred/src/transport.rs
274 @@ -207,31 +207,31 @@ mod test {
275 let mut transport = Transport::default();
276 match transport.decode(&mut BytesMut::from("HELO example.org\r\n")) {
277 Ok(Some(command)) => assert!(matches!(command, Command::Requests(_))),
278- _ => panic!()
279+ _ => panic!(),
280 };
281 match transport.decode(&mut BytesMut::from("DATA\r\n")) {
282 Ok(Some(command)) => assert!(matches!(command, Command::Requests(_))),
283- _ => panic!()
284+ _ => panic!(),
285 };
286 match transport.decode(&mut BytesMut::from("Subject: Hello World\r\n")) {
287- Ok(None) => {},
288- _ => panic!()
289+ Ok(None) => {}
290+ _ => panic!(),
291 };
292 match transport.decode(&mut BytesMut::from("AAAAAAABBBBBBCCCCCCC")) {
293- Ok(None) => {},
294- _ => panic!()
295+ Ok(None) => {}
296+ _ => panic!(),
297 };
298 match transport.decode(&mut BytesMut::from("DDDDDDDEEEEEEEFFFFFF")) {
299- Ok(None) => {},
300- _ => panic!()
301+ Ok(None) => {}
302+ _ => panic!(),
303 };
304 match transport.decode(&mut BytesMut::from("\r\n.\r\n")) {
305- Ok(Some(Command::Payload(_))) => {},
306- _ => panic!()
307+ Ok(Some(Command::Payload(_))) => {}
308+ _ => panic!(),
309 };
310 match transport.decode(&mut BytesMut::from("QUIT\r\n")) {
311- Ok(Some(Command::Requests(_))) => {},
312- _ => panic!()
313+ Ok(Some(Command::Requests(_))) => {}
314+ _ => panic!(),
315 };
316 }
317
318 @@ -240,32 +240,31 @@ mod test {
319 let mut transport = Transport::default().pipelining(true);
320 match transport.decode(&mut BytesMut::from("HELO example.org\r\n")) {
321 Ok(Some(command)) => assert!(matches!(command, Command::Requests(_))),
322- _ => panic!()
323+ _ => panic!(),
324 };
325 match transport.decode(&mut BytesMut::from("DATA\r\n")) {
326 Ok(Some(command)) => assert!(matches!(command, Command::Requests(_))),
327- _ => panic!()
328+ _ => panic!(),
329 };
330 match transport.decode(&mut BytesMut::from("Subject: Hello World\r\n")) {
331- Ok(None) => {},
332- _ => panic!()
333+ Ok(None) => {}
334+ _ => panic!(),
335 };
336 match transport.decode(&mut BytesMut::from("AAAAAAABBBBBBCCCCCCC")) {
337- Ok(None) => {},
338- _ => panic!()
339+ Ok(None) => {}
340+ _ => panic!(),
341 };
342 match transport.decode(&mut BytesMut::from("DDDDDDDEEEEEEEFFFFFF")) {
343- Ok(None) => {},
344- _ => panic!()
345+ Ok(None) => {}
346+ _ => panic!(),
347 };
348 match transport.decode(&mut BytesMut::from("\r\n.\r\n")) {
349- Ok(Some(Command::Payload(_))) => {},
350- _ => panic!()
351+ Ok(Some(Command::Payload(_))) => {}
352+ _ => panic!(),
353 };
354 match transport.decode(&mut BytesMut::from("QUIT\r\n")) {
355- Ok(Some(Command::Requests(_))) => {},
356- _ => panic!()
357+ Ok(Some(Command::Requests(_))) => {}
358+ _ => panic!(),
359 };
360 }
361-
362 }