Commit
Author: Kevin Schoon [me@kevinschoon.com]
Hash: 01de4f37207d3c87cacf7830114dd281260f2893
Timestamp: Wed, 09 Oct 2024 16:12:03 +0000 (2 months ago)

+146 -337 +/-5 browse
move spf validation out of session
1diff --git a/README.md b/README.md
2index 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
15index 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
37index 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
255index 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
830index 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 => {