Author:
Hash:
Timestamp:
+146 -337 +/-5 browse
Kevin Schoon [me@kevinschoon.com]
01de4f37207d3c87cacf7830114dd281260f2893
Wed, 09 Oct 2024 16:12:03 +0000 (1.0 years ago)
| 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 => { |