Commit
+146 -337 +/-5 browse
1 | diff --git a/README.md b/README.md |
2 | index 96fdb72..0420f0f 100644 |
3 | --- a/README.md |
4 | +++ b/README.md |
5 | @@ -71,7 +71,7 @@ All authentication extensions are implemented with the |
6 | |---------------------------|--------| |
7 | | DKIM Verification | ✅ | |
8 | | ARC Chain Verification | TODO | |
9 | - | SPF Policy Evaluation | TODO | |
10 | + | SPF Policy Evaluation | ✅ | |
11 | | DMARC Policy Evaluation | TODO | |
12 | |
13 | |
14 | diff --git a/maitred.toml b/maitred.toml |
15 | index 0dac335..7c133b3 100644 |
16 | --- a/maitred.toml |
17 | +++ b/maitred.toml |
18 | @@ -5,7 +5,7 @@ maildir = "mail" |
19 | hostname = "localhost:2525" |
20 | |
21 | # logging level |
22 | - level = "DEBUG" |
23 | + level = "TRACE" |
24 | |
25 | # address to bind to |
26 | address = "0.0.0.0:2525" |
27 | @@ -21,7 +21,7 @@ key = "key.pem" |
28 | enabled = false |
29 | |
30 | [spf] |
31 | - enabled = true |
32 | + enabled = false |
33 | |
34 | [[accounts]] |
35 | address = "demo-1@example.org" |
36 | diff --git a/maitred/src/server.rs b/maitred/src/server.rs |
37 | index b9ef15d..3ece38c 100644 |
38 | --- a/maitred/src/server.rs |
39 | +++ b/maitred/src/server.rs |
40 | @@ -1,6 +1,6 @@ |
41 | use std::fs::File as StdFile; |
42 | use std::io::BufReader as StdBufReader; |
43 | - use std::net::SocketAddr; |
44 | + use std::net::{IpAddr, SocketAddr}; |
45 | use std::path::{Path, PathBuf}; |
46 | use std::sync::Arc; |
47 | use std::time::Duration; |
48 | @@ -62,7 +62,7 @@ pub enum ServerError { |
49 | /// Action for controlling a TCP session |
50 | pub(crate) enum Action { |
51 | Continue, |
52 | - Enqueue, |
53 | + Enqueue(Envelope), |
54 | Shutdown, |
55 | TlsUpgrade, |
56 | } |
57 | @@ -225,9 +225,31 @@ impl Server { |
58 | .with_single_cert(certs, private_key)?) |
59 | } |
60 | |
61 | + async fn verify(&self, client_ip: IpAddr, envelope: &Envelope) -> Option<Response<String>> { |
62 | + if !self.spf_verification { |
63 | + return None; |
64 | + } |
65 | + let resolver = self.resolver.as_ref().expect("resolver not configured"); |
66 | + let resolver = resolver.lock().await; |
67 | + if !Validation(resolver) |
68 | + .verify_spf( |
69 | + client_ip, |
70 | + &envelope.hostname.to_string(), |
71 | + &self.our_hostname, |
72 | + envelope.mail_from.as_str(), |
73 | + ) |
74 | + .await |
75 | + { |
76 | + return Some(crate::session::spf_rejection()); |
77 | + } |
78 | + // TODO DKIM verification here instead of worker? |
79 | + None |
80 | + } |
81 | + |
82 | /// drive the session forward |
83 | - async fn on_frame( |
84 | + async fn next( |
85 | &self, |
86 | + client_ip: IpAddr, |
87 | conn: impl Opportunistic, |
88 | session: &mut Session, |
89 | ) -> Result<Action, ServerError> { |
90 | @@ -244,7 +266,7 @@ impl Server { |
91 | conn.send(response).await?; |
92 | } |
93 | } |
94 | - crate::session::Action::BDat { |
95 | + crate::session::Action::Message { |
96 | initial_response, |
97 | cb, |
98 | } => { |
99 | @@ -254,51 +276,24 @@ impl Server { |
100 | crate::session::Action::Send(response) => { |
101 | conn.send(response).await?; |
102 | } |
103 | - _ => unreachable!(), |
104 | - }, |
105 | - _ => unreachable!(), |
106 | - } |
107 | - } |
108 | - crate::session::Action::Data { |
109 | - initial_response, |
110 | - cb, |
111 | - } => { |
112 | - conn.send(initial_response).await?; |
113 | - match conn.next().await { |
114 | - Some(Ok(Command::Payload(payload))) => match cb(payload) { |
115 | - crate::session::Action::Send(response) => { |
116 | - conn.send(response).await?; |
117 | - return Ok(Action::Enqueue); |
118 | + crate::session::Action::Envelope { |
119 | + initial_response, |
120 | + envelope, |
121 | + } => { |
122 | + if let Some(err_msg) = |
123 | + self.verify(client_ip, &envelope).await |
124 | + { |
125 | + conn.send(err_msg).await?; |
126 | + return Ok(Action::Shutdown); |
127 | + } |
128 | + conn.send(initial_response).await?; |
129 | + return Ok(Action::Enqueue(envelope)); |
130 | } |
131 | _ => unreachable!(), |
132 | }, |
133 | _ => unreachable!(), |
134 | } |
135 | } |
136 | - crate::session::Action::SpfVerification { |
137 | - ip_addr, |
138 | - helo_domain, |
139 | - host_domain, |
140 | - mail_from, |
141 | - cb, |
142 | - } => { |
143 | - let resolver = self.resolver.as_ref().expect("resolver not configured"); |
144 | - let resolver = resolver.lock().await; |
145 | - match cb(Validation(resolver) |
146 | - .verify_spf(ip_addr, &helo_domain, &host_domain, mail_from.as_str()) |
147 | - .await) |
148 | - { |
149 | - crate::session::Action::Send(response) => { |
150 | - conn.send(response).await?; |
151 | - return Ok(Action::Continue); |
152 | - } |
153 | - crate::session::Action::Quit(response) => { |
154 | - conn.send(response).await?; |
155 | - return Ok(Action::Shutdown); |
156 | - } |
157 | - _ => unreachable!(), |
158 | - } |
159 | - } |
160 | crate::session::Action::PlainAuth { |
161 | authcid, |
162 | authzid, |
163 | @@ -312,7 +307,6 @@ impl Server { |
164 | match cb(plain_auth.authenticate(&authcid, &authzid, &password).await) { |
165 | crate::session::Action::Send(response) => { |
166 | conn.send(response).await?; |
167 | - return Ok(Action::Continue); |
168 | } |
169 | _ => unreachable!(), |
170 | } |
171 | @@ -325,7 +319,6 @@ impl Server { |
172 | match cb(verification.verify(&address).await) { |
173 | crate::session::Action::Send(response) => { |
174 | conn.send(response).await?; |
175 | - return Ok(Action::Continue); |
176 | } |
177 | _ => unreachable!(), |
178 | } |
179 | @@ -338,7 +331,6 @@ impl Server { |
180 | match cb(expansion.expand(&address).await) { |
181 | crate::session::Action::Send(response) => { |
182 | conn.send(response).await?; |
183 | - return Ok(Action::Continue); |
184 | } |
185 | _ => unreachable!(), |
186 | } |
187 | @@ -352,6 +344,10 @@ impl Server { |
188 | conn.send(response).await?; |
189 | return Ok(Action::Shutdown); |
190 | } |
191 | + crate::session::Action::Envelope { |
192 | + initial_response: _, |
193 | + envelope: _, |
194 | + } => unreachable!(), |
195 | } |
196 | } |
197 | Ok(Action::Continue) |
198 | @@ -394,12 +390,10 @@ impl Server { |
199 | let mut session = self |
200 | .session |
201 | .clone() |
202 | - .client_ip(remote_addr.ip()) |
203 | .our_hostname(&self.our_hostname) |
204 | .starttls(self.tls_certificates.is_some()) |
205 | .vrfy_enabled(self.verification.is_some()) |
206 | - .expn_enabled(self.list_expansion.is_some()) |
207 | - .spf_verification(self.spf_verification); |
208 | + .expn_enabled(self.list_expansion.is_some()); |
209 | |
210 | let mut framed = Framed::new( |
211 | &mut *stream, |
212 | @@ -418,7 +412,8 @@ impl Server { |
213 | |
214 | loop { |
215 | match self |
216 | - .on_frame( |
217 | + .next( |
218 | + remote_addr.ip(), |
219 | Plain { |
220 | inner: framed.clone(), |
221 | }, |
222 | @@ -427,8 +422,8 @@ impl Server { |
223 | .await? |
224 | { |
225 | Action::Continue => {} |
226 | - Action::Enqueue => { |
227 | - msg_queue.push(session.envelope()); |
228 | + Action::Enqueue(envelope) => { |
229 | + msg_queue.push(envelope); |
230 | } |
231 | Action::Shutdown => return Ok(()), |
232 | Action::TlsUpgrade => { |
233 | @@ -442,7 +437,8 @@ impl Server { |
234 | let mut session = session.clone().tls_active(true); |
235 | loop { |
236 | match self |
237 | - .on_frame( |
238 | + .next( |
239 | + remote_addr.ip(), |
240 | Tls { |
241 | inner: tls_framed.clone(), |
242 | }, |
243 | @@ -451,8 +447,8 @@ impl Server { |
244 | .await? |
245 | { |
246 | Action::Continue => {} |
247 | - Action::Enqueue => { |
248 | - msg_queue.push(session.envelope()); |
249 | + Action::Enqueue(envelope) => { |
250 | + msg_queue.push(envelope); |
251 | } |
252 | Action::Shutdown => return Ok(()), |
253 | Action::TlsUpgrade => unreachable!(), |
254 | diff --git a/maitred/src/session.rs b/maitred/src/session.rs |
255 | index 2b29fdf..2f1008b 100644 |
256 | --- a/maitred/src/session.rs |
257 | +++ b/maitred/src/session.rs |
258 | @@ -1,5 +1,4 @@ |
259 | use std::fmt::Display; |
260 | - use std::net::IpAddr; |
261 | use std::str::FromStr; |
262 | |
263 | use bytes::Bytes; |
264 | @@ -98,20 +97,13 @@ pub struct Envelope { |
265 | pub enum Action<'a> { |
266 | Send(Response<String>), |
267 | SendMany(Vec<Response<String>>), |
268 | - BDat { |
269 | + Message { |
270 | initial_response: Response<String>, |
271 | cb: Box<dyn FnOnce(Bytes) -> Action<'a> + 'a>, |
272 | }, |
273 | - Data { |
274 | + Envelope { |
275 | initial_response: Response<String>, |
276 | - cb: Box<dyn FnOnce(Bytes) -> Action<'a> + 'a>, |
277 | - }, |
278 | - SpfVerification { |
279 | - ip_addr: IpAddr, |
280 | - helo_domain: String, |
281 | - host_domain: String, |
282 | - mail_from: EmailAddress, |
283 | - cb: Box<dyn FnOnce(bool) -> Action<'a> + 'a>, |
284 | + envelope: Envelope, |
285 | }, |
286 | PlainAuth { |
287 | authcid: String, |
288 | @@ -152,29 +144,20 @@ impl Display for Action<'_> { |
289 | }); |
290 | Ok(()) |
291 | } |
292 | - Action::BDat { |
293 | + Action::Message { |
294 | initial_response, |
295 | cb: _, |
296 | } => match initial_response { |
297 | - Response::General(response) => f.write_fmt(format_args!("BDat:\n{}", response)), |
298 | + Response::General(response) => f.write_fmt(format_args!("Message:\n{}", response)), |
299 | Response::Ehlo(_ehlo_response) => unreachable!(), |
300 | }, |
301 | - Action::Data { |
302 | + Action::Envelope { |
303 | initial_response, |
304 | - cb: _, |
305 | - } => match initial_response { |
306 | - Response::General(response) => f.write_fmt(format_args!("Data:\n{}", response)), |
307 | - Response::Ehlo(_ehlo_response) => unreachable!(), |
308 | - }, |
309 | - Action::SpfVerification { |
310 | - ip_addr, |
311 | - helo_domain, |
312 | - host_domain, |
313 | - mail_from, |
314 | - cb: _, |
315 | + envelope, |
316 | } => f.write_fmt(format_args!( |
317 | - "Spf: ip={}, domain={}, us={}, mail={}", |
318 | - ip_addr, helo_domain, host_domain, mail_from |
319 | + "Envelope: {:?}, {}", |
320 | + initial_response, |
321 | + envelope.mail_from.to_string() |
322 | )), |
323 | Action::PlainAuth { |
324 | authcid, |
325 | @@ -218,6 +201,10 @@ pub fn tls_already_active() -> Response<String> { |
326 | smtp_response!(400, 0, 0, 0, "TLS is already active") |
327 | } |
328 | |
329 | + pub fn spf_rejection() -> Response<String> { |
330 | + smtp_response!(500, 0, 0, 0, "SPF Verification Failed") |
331 | + } |
332 | + |
333 | pub fn smtp_error_to_response(e: smtp_proto::Error) -> Response<String> { |
334 | match e { |
335 | smtp_proto::Error::NeedsMoreData { bytes_left: _ } => { |
336 | @@ -280,20 +267,19 @@ struct Flags { |
337 | starttls: bool, |
338 | vrfy: bool, |
339 | expn: bool, |
340 | - spf: bool, |
341 | } |
342 | |
343 | /// State machine that corresponds to a single SMTP session, calls to next |
344 | /// return actions that the caller is expected to implement in a transport. |
345 | #[derive(Clone)] |
346 | pub struct Session { |
347 | - /// message body |
348 | - pub body: Option<Message<'static>>, |
349 | /// mailto address |
350 | - pub mail_from: Option<EmailAddress>, |
351 | + mail_from: Option<EmailAddress>, |
352 | /// rcpt address |
353 | - pub rcpt_to: Option<Vec<EmailAddress>>, |
354 | - pub hostname: Option<Host>, |
355 | + rcpt_to: Option<Vec<EmailAddress>>, |
356 | + /// hostname per HELO |
357 | + hostname: Option<Host>, |
358 | + |
359 | initialized: Option<Mode>, |
360 | // previously ran commands |
361 | // TODO pipeline still partially broken |
362 | @@ -301,7 +287,6 @@ pub struct Session { |
363 | |
364 | // session opts |
365 | our_hostname: Option<String>, // required |
366 | - client_ip: Option<IpAddr>, |
367 | maximum_size: u64, |
368 | capabilities: u32, |
369 | help_banner: String, |
370 | @@ -316,14 +301,12 @@ pub struct Session { |
371 | impl Default for Session { |
372 | fn default() -> Self { |
373 | Session { |
374 | - body: None, |
375 | mail_from: None, |
376 | rcpt_to: None, |
377 | hostname: None, |
378 | initialized: None, |
379 | history: Vec::new(), |
380 | our_hostname: None, |
381 | - client_ip: None, |
382 | maximum_size: DEFAULT_MAXIMUM_MESSAGE_SIZE, |
383 | capabilities: DEFAULT_CAPABILITIES, |
384 | help_banner: DEFAULT_HELP_BANNER.to_string(), |
385 | @@ -342,11 +325,6 @@ impl Session { |
386 | self |
387 | } |
388 | |
389 | - pub fn spf_verification(mut self, verify_spf: bool) -> Self { |
390 | - self.flags.spf = verify_spf; |
391 | - self |
392 | - } |
393 | - |
394 | pub fn maximum_size(mut self, maximum_size: u64) -> Self { |
395 | self.maximum_size = maximum_size; |
396 | self |
397 | @@ -373,11 +351,6 @@ impl Session { |
398 | self |
399 | } |
400 | |
401 | - pub fn client_ip(mut self, client_ip: IpAddr) -> Self { |
402 | - self.client_ip = Some(client_ip); |
403 | - self |
404 | - } |
405 | - |
406 | pub fn starttls(mut self, enabled: bool) -> Self { |
407 | self.flags.starttls = enabled; |
408 | self.capabilities |= smtp_proto::EXT_START_TLS; |
409 | @@ -402,7 +375,6 @@ impl Session { |
410 | /// Reset the connection to it's default state but after a HELO/ELHO has |
411 | /// been issued successfully. |
412 | pub fn reset(&mut self) { |
413 | - self.body = None; |
414 | self.mail_from = None; |
415 | self.rcpt_to = None; |
416 | // FIXME: is the hostname reset? |
417 | @@ -464,12 +436,32 @@ impl Session { |
418 | Ok(()) |
419 | } |
420 | |
421 | - pub fn envelope(&self) -> Envelope { |
422 | - Envelope { |
423 | - body: self.body.clone().unwrap(), |
424 | - mail_from: self.mail_from.clone().unwrap(), |
425 | - rcpt_to: self.rcpt_to.clone().unwrap(), |
426 | - hostname: self.hostname.clone().unwrap(), |
427 | + /// Called each time a message is ready for processing, will do spf |
428 | + /// validation if it is configured. |
429 | + fn accept_payload(&mut self, payload: Bytes) -> Action<'_> { |
430 | + if self.rcpt_to.is_none() { |
431 | + return Action::Send(smtp_response!(500, 0, 0, 0, "RCPT TO is missing")); |
432 | + } |
433 | + if self.hostname.is_none() { |
434 | + return Action::Send(smtp_response!(500, 0, 0, 0, "Hostname is missing")); |
435 | + } |
436 | + let copied = payload.to_vec(); |
437 | + if let Err(response) = self.check_body(&copied) { |
438 | + return Action::Send(response); |
439 | + }; |
440 | + let parser = MessageParser::new(); |
441 | + match parser.parse(&copied) { |
442 | + Some(message) => Action::Envelope { |
443 | + initial_response: smtp_response!(250, 0, 0, 0, "OK"), |
444 | + envelope: Envelope { |
445 | + body: message.into_owned(), |
446 | + // FIXME |
447 | + mail_from: self.mail_from.clone().unwrap(), |
448 | + rcpt_to: self.rcpt_to.clone().unwrap(), |
449 | + hostname: self.hostname.clone().unwrap(), |
450 | + }, |
451 | + }, |
452 | + None => Action::Send(smtp_response!(500, 0, 0, 0, "Cannot parse message payload")), |
453 | } |
454 | } |
455 | |
456 | @@ -550,60 +542,7 @@ impl Session { |
457 | } |
458 | }; |
459 | self.mail_from = Some(mail_from.clone()); |
460 | - if self.flags.spf { |
461 | - tracing::info!("Running SPF Validation"); |
462 | - let ip_addr = match self.client_ip { |
463 | - Some(ip_addr) => ip_addr, |
464 | - None => { |
465 | - return Action::Quit(smtp_response!( |
466 | - 500, |
467 | - 0, |
468 | - 0, |
469 | - 0, |
470 | - "Client has no IP Address" |
471 | - )) |
472 | - } |
473 | - }; |
474 | - let helo_domain = match &self.hostname { |
475 | - Some(helo_domain) => helo_domain.to_string(), |
476 | - None => { |
477 | - return Action::Quit(smtp_response!( |
478 | - 500, |
479 | - 0, |
480 | - 0, |
481 | - 0, |
482 | - "hostname is not specified" |
483 | - )) |
484 | - } |
485 | - }; |
486 | - let host_domain = self |
487 | - .our_hostname |
488 | - .clone() |
489 | - .expect("session hostname not specified"); |
490 | - let inner = self; |
491 | - Action::SpfVerification { |
492 | - ip_addr, |
493 | - helo_domain: helo_domain.clone(), |
494 | - host_domain, |
495 | - mail_from: mail_from.clone(), |
496 | - cb: Box::new(move |success| { |
497 | - if success { |
498 | - inner.spf_verified_host = Some(helo_domain.clone()); |
499 | - Action::Send(smtp_response!(250, 0, 0, 0, "OK")) |
500 | - } else { |
501 | - Action::Quit(smtp_response!( |
502 | - 500, |
503 | - 0, |
504 | - 0, |
505 | - 0, |
506 | - "SPF Verification Failed" |
507 | - )) |
508 | - } |
509 | - }), |
510 | - } |
511 | - } else { |
512 | - Action::Send(smtp_response!(250, 0, 0, 0, "OK")) |
513 | - } |
514 | + Action::Send(smtp_response!(250, 0, 0, 0, "OK")) |
515 | } |
516 | Some(Request::Rcpt { to }) => { |
517 | if let Some(err) = self.check_initialized().err() { |
518 | @@ -637,7 +576,7 @@ impl Session { |
519 | } |
520 | let inner = self; |
521 | tracing::info!("Starting binary data transfer"); |
522 | - Action::BDat { |
523 | + Action::Message { |
524 | initial_response: smtp_response!( |
525 | 354, |
526 | 0, |
527 | @@ -645,26 +584,7 @@ impl Session { |
528 | 0, |
529 | "Starting BDAT data transfer".to_string() |
530 | ), |
531 | - cb: Box::new(move |payload| { |
532 | - let copied = payload.to_vec(); |
533 | - if let Err(response) = inner.check_body(&copied) { |
534 | - return Action::Send(response); |
535 | - }; |
536 | - let parser = MessageParser::new(); |
537 | - match parser.parse(&copied) { |
538 | - Some(message) => { |
539 | - inner.body = Some(message.into_owned()); |
540 | - Action::Send(smtp_response!(250, 0, 0, 0, "OK")) |
541 | - } |
542 | - None => Action::Send(smtp_response!( |
543 | - 500, |
544 | - 0, |
545 | - 0, |
546 | - 0, |
547 | - "Cannot parse message payload" |
548 | - )), |
549 | - } |
550 | - }), |
551 | + cb: Box::new(move |payload| inner.accept_payload(payload.to_vec().into())), |
552 | } |
553 | } |
554 | // After an AUTH command has been successfully completed, no more |
555 | @@ -811,7 +731,7 @@ impl Session { |
556 | } |
557 | tracing::info!("Starting data transfer"); |
558 | let inner = self; |
559 | - Action::Data { |
560 | + Action::Message { |
561 | initial_response: smtp_response!( |
562 | 354, |
563 | 0, |
564 | @@ -819,26 +739,7 @@ impl Session { |
565 | 0, |
566 | "Reading data input, end the message with <CRLF>.<CRLF>".to_string() |
567 | ), |
568 | - cb: Box::new(move |payload| { |
569 | - let copied = payload.to_vec(); |
570 | - if let Err(response) = inner.check_body(&copied) { |
571 | - return Action::Send(response); |
572 | - }; |
573 | - let parser = MessageParser::new(); |
574 | - match parser.parse(&copied) { |
575 | - Some(message) => { |
576 | - inner.body = Some(message.into_owned()); |
577 | - Action::Send(smtp_response!(250, 0, 0, 0, "OK")) |
578 | - } |
579 | - None => Action::Send(smtp_response!( |
580 | - 500, |
581 | - 0, |
582 | - 0, |
583 | - 0, |
584 | - "Cannot parse message payload" |
585 | - )), |
586 | - } |
587 | - }), |
588 | + cb: Box::new(move |payload| inner.accept_payload(payload.to_vec().into())), |
589 | } |
590 | } |
591 | Some(Request::Rset) => { |
592 | @@ -856,8 +757,6 @@ impl Session { |
593 | #[cfg(test)] |
594 | mod test { |
595 | |
596 | - use std::net::Ipv4Addr; |
597 | - |
598 | use base64::engine::general_purpose::STANDARD; |
599 | use base64::prelude::*; |
600 | use smtp_proto::MailFrom; |
601 | @@ -881,21 +780,22 @@ mod test { |
602 | }), |
603 | _ => false, |
604 | }, |
605 | - Action::BDat { |
606 | + Action::Message { |
607 | initial_response: _, |
608 | cb: _, |
609 | } => todo!(), |
610 | - Action::Data { |
611 | + Action::Envelope { |
612 | initial_response: _, |
613 | - cb: _, |
614 | - } => todo!(), |
615 | - Action::SpfVerification { |
616 | - ip_addr: _, |
617 | - helo_domain: _, |
618 | - host_domain: _, |
619 | - mail_from: _, |
620 | - cb: _, |
621 | - } => todo!(), |
622 | + envelope: _, |
623 | + } => { |
624 | + matches!( |
625 | + expected, |
626 | + Action::Envelope { |
627 | + initial_response: _, |
628 | + envelope: _ |
629 | + } |
630 | + ) |
631 | + } |
632 | Action::PlainAuth { |
633 | authcid: _, |
634 | authzid: _, |
635 | @@ -955,103 +855,6 @@ mod test { |
636 | } |
637 | |
638 | #[test] |
639 | - fn session_spf_successful() { |
640 | - let mut session = Session::default() |
641 | - .our_hostname("localhost:2525") |
642 | - .spf_verification(true) |
643 | - .client_ip(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))); |
644 | - assert!(equal( |
645 | - &session.next(Some(&Request::Helo { |
646 | - host: EXAMPLE_HOSTNAME.to_string(), |
647 | - })), |
648 | - &Action::Send(smtp_response!( |
649 | - 250, |
650 | - 0, |
651 | - 0, |
652 | - 0, |
653 | - String::from("Hello example.org") |
654 | - )), |
655 | - )); |
656 | - match session.next(Some(&Request::Mail { |
657 | - from: MailFrom { |
658 | - address: String::from("fuu@example.org"), |
659 | - ..Default::default() |
660 | - }, |
661 | - })) { |
662 | - Action::SpfVerification { |
663 | - ip_addr, |
664 | - helo_domain, |
665 | - host_domain, |
666 | - mail_from, |
667 | - cb, |
668 | - } => { |
669 | - assert!(ip_addr.eq(&IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)))); |
670 | - assert!(helo_domain.eq(EXAMPLE_HOSTNAME)); |
671 | - assert!(host_domain.eq("localhost:2525")); |
672 | - assert!(mail_from.as_str().eq("fuu@example.org")); |
673 | - assert!(equal( |
674 | - &cb(true), |
675 | - &Action::Send(smtp_response!(250, 0, 0, 0, "OK")) |
676 | - )) |
677 | - } |
678 | - _ => { |
679 | - unreachable!(); |
680 | - } |
681 | - } |
682 | - |
683 | - assert!(session |
684 | - .hostname |
685 | - .as_ref() |
686 | - .is_some_and(|hostname| hostname.to_string() == EXAMPLE_HOSTNAME)); |
687 | - } |
688 | - |
689 | - #[test] |
690 | - fn session_spf_failed() { |
691 | - let mut session = Session::default() |
692 | - .our_hostname("localhost:2525") |
693 | - .spf_verification(true) |
694 | - .client_ip(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))); |
695 | - assert!(equal( |
696 | - &session.next(Some(&Request::Helo { |
697 | - host: EXAMPLE_HOSTNAME.to_string(), |
698 | - })), |
699 | - &Action::Send(smtp_response!( |
700 | - 250, |
701 | - 0, |
702 | - 0, |
703 | - 0, |
704 | - String::from("Hello example.org") |
705 | - )), |
706 | - )); |
707 | - match session.next(Some(&Request::Mail { |
708 | - from: MailFrom { |
709 | - address: String::from("fuu@example.org"), |
710 | - ..Default::default() |
711 | - }, |
712 | - })) { |
713 | - Action::SpfVerification { |
714 | - ip_addr, |
715 | - helo_domain, |
716 | - host_domain, |
717 | - mail_from, |
718 | - cb, |
719 | - } => { |
720 | - assert!(ip_addr.eq(&IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)))); |
721 | - assert!(helo_domain.eq(EXAMPLE_HOSTNAME)); |
722 | - assert!(host_domain.eq("localhost:2525")); |
723 | - assert!(mail_from.as_str().eq("fuu@example.org")); |
724 | - assert!(equal( |
725 | - &cb(false), |
726 | - &Action::Quit(smtp_response!(500, 0, 0, 0, "SPF Verification Failed")) |
727 | - )) |
728 | - } |
729 | - _ => { |
730 | - unreachable!(); |
731 | - } |
732 | - }; |
733 | - } |
734 | - |
735 | - #[test] |
736 | fn session_command_with_no_helo() { |
737 | let mut session = Session::default(); |
738 | assert!(equal( |
739 | @@ -1166,9 +969,11 @@ mod test { |
740 | let session = &mut Session::default(); |
741 | // non-extended sessions cannot accept non-ascii characters |
742 | session.initialized = Some(Mode::Legacy); |
743 | + session.hostname = Some(Host::Domain(String::from("bar.com"))); |
744 | session.mail_from = Some(EmailAddress::new_unchecked("fuu@bar.com")); |
745 | + session.rcpt_to = Some(vec![EmailAddress::new_unchecked("qux@baz.com")]); |
746 | match session.next(Some(&Request::Data {})) { |
747 | - Action::Data { |
748 | + Action::Message { |
749 | initial_response, |
750 | cb, |
751 | } => { |
752 | @@ -1210,8 +1015,10 @@ Subject: Hello World |
753 | // non-extended sessions cannot accept non-ascii characters |
754 | session.initialized = Some(Mode::Extended); |
755 | session.mail_from = Some(EmailAddress::new_unchecked("fuu@bar.com")); |
756 | + session.hostname = Some(Host::Domain(String::from("bar.com"))); |
757 | + session.rcpt_to = Some(vec![EmailAddress::new_unchecked("qux@baz.com")]); |
758 | match session.next(Some(&Request::Data {})) { |
759 | - Action::Data { |
760 | + Action::Message { |
761 | initial_response, |
762 | cb, |
763 | } => { |
764 | @@ -1234,7 +1041,15 @@ Subject: Hello World |
765 | )); |
766 | assert!(equal( |
767 | &action, |
768 | - &Action::Send(smtp_response!(250, 0, 0, 0, "OK")) |
769 | + &Action::Envelope { |
770 | + initial_response: smtp_response!(250, 0, 0, 0, "OK"), |
771 | + envelope: Envelope { |
772 | + body: Message::default(), |
773 | + mail_from: EmailAddress::new_unchecked("fuu@bar.com"), |
774 | + rcpt_to: vec![], |
775 | + hostname: Host::Domain(String::from("bar.com")) |
776 | + } |
777 | + } |
778 | )) |
779 | } |
780 | _ => panic!("Unexpected response"), |
781 | @@ -1246,10 +1061,12 @@ Subject: Hello World |
782 | let session = &mut Session::default(); |
783 | // non-extended sessions cannot accept non-ascii characters |
784 | session.initialized = Some(Mode::Extended); |
785 | + session.hostname = Some(Host::Domain(String::from("bar.com"))); |
786 | + session.rcpt_to = Some(vec![EmailAddress::new_unchecked("qux@baz.com")]); |
787 | session.mail_from = Some(EmailAddress::new_unchecked("fuu@bar.com")); |
788 | { |
789 | match session.next(Some(&Request::Data {})) { |
790 | - Action::Data { |
791 | + Action::Message { |
792 | initial_response, |
793 | cb, |
794 | } => { |
795 | @@ -1276,23 +1093,19 @@ transport rather than the session. 🩷 |
796 | )); |
797 | assert!(equal( |
798 | &action, |
799 | - &Action::Send(smtp_response!(250, 0, 0, 0, "OK")) |
800 | - )) |
801 | + &Action::Envelope { |
802 | + initial_response: smtp_response!(250, 0, 0, 0, "OK"), |
803 | + envelope: Envelope { |
804 | + body: Message::default(), |
805 | + mail_from: EmailAddress::new_unchecked("fuu@bar.com"), |
806 | + rcpt_to: vec![], |
807 | + hostname: Host::Domain("example.org".to_string()) |
808 | + } |
809 | + } |
810 | + )); |
811 | } |
812 | _ => panic!("Unexpected response"), |
813 | }; |
814 | }; |
815 | - |
816 | - let message_body = session.body.clone().unwrap(); |
817 | - |
818 | - assert!(message_body |
819 | - .to() |
820 | - .is_some_and(|to| to.first().is_some_and(|to| to |
821 | - .address |
822 | - .as_ref() |
823 | - .is_some_and(|addr| { addr == "baz@qux.com" })))); |
824 | - assert!(message_body |
825 | - .subject() |
826 | - .is_some_and(|subject| subject == "Hello World")); |
827 | } |
828 | } |
829 | diff --git a/maitred/src/validation.rs b/maitred/src/validation.rs |
830 | index 12e65c5..b81de23 100644 |
831 | --- a/maitred/src/validation.rs |
832 | +++ b/maitred/src/validation.rs |
833 | @@ -45,11 +45,11 @@ impl Validation<'_> { |
834 | ip: IpAddr, |
835 | helo_domain: &str, |
836 | host_domain: &str, |
837 | - sender: &str, |
838 | + mail_from: &str, |
839 | ) -> bool { |
840 | let output = self |
841 | .0 |
842 | - .verify_spf(ip, helo_domain, host_domain, sender) |
843 | + .verify_spf(ip, helo_domain, host_domain, mail_from) |
844 | .await; |
845 | match output.result() { |
846 | mail_auth::SpfResult::Pass => { |