1mod data;
4
5use crate::command::SmtpCommand;
6use crate::parser::parse_command_smtputf8;
7use crate::response::SmtpResponse;
8use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
9use ipnetwork::IpNetwork;
10use rusmes_auth::AuthBackend;
11use rusmes_core::{MailProcessorRouter, RateLimiter};
12use rusmes_proto::{MailAddress, Username};
13use rusmes_storage::StorageBackend;
14use std::collections::HashMap;
15use std::net::SocketAddr;
16use std::sync::Arc;
17use std::time::{Duration, Instant};
18use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter};
19use tokio::net::TcpStream;
20use tokio::sync::RwLock;
21
22#[derive(Debug, Clone, PartialEq)]
24pub enum SmtpState {
25 Initial,
27 Connected,
29 Authenticated,
31 MailTransaction,
33 Data,
35 Quit,
37}
38
39#[derive(Debug, Clone)]
41pub struct SmtpTransaction {
42 sender: Option<MailAddress>,
43 recipients: Vec<MailAddress>,
44 helo_name: Option<String>,
45 message_size: usize,
46 declared_size: Option<usize>,
48 body_type: Option<String>,
50 smtputf8: bool,
52 bdat_state: Option<crate::BdatState>,
54 message_data: Vec<u8>,
56}
57
58impl SmtpTransaction {
59 fn new() -> Self {
60 Self {
61 sender: None,
62 recipients: Vec::new(),
63 helo_name: None,
64 message_size: 0,
65 declared_size: None,
66 body_type: None,
67 smtputf8: false,
68 bdat_state: None,
69 message_data: Vec::new(),
70 }
71 }
72
73 fn reset(&mut self) {
74 self.sender = None;
75 self.recipients.clear();
76 self.message_size = 0;
77 self.declared_size = None;
78 self.body_type = None;
79 self.smtputf8 = false;
80 self.bdat_state = None;
81 self.message_data.clear();
82 }
83
84 fn is_valid(&self) -> bool {
85 self.sender.is_some() && !self.recipients.is_empty()
86 }
87}
88
89#[derive(Debug, Clone)]
91pub struct SmtpConfig {
92 pub hostname: String,
93 pub max_message_size: usize,
94 pub require_auth: bool,
95 pub enable_starttls: bool,
96 pub check_recipient_exists: bool,
97 pub reject_unknown_recipients: bool,
98 pub relay_networks: Vec<String>,
100 pub local_domains: Vec<String>,
102 pub connection_timeout: Duration,
104 pub idle_timeout: Duration,
106 pub blocked_networks: Vec<IpNetwork>,
109 pub data_tempfile_threshold: usize,
115 pub data_spill_dir: std::path::PathBuf,
121}
122
123impl Default for SmtpConfig {
124 fn default() -> Self {
125 Self {
126 hostname: "localhost".to_string(),
127 max_message_size: 10 * 1024 * 1024, require_auth: false,
129 enable_starttls: false,
130 check_recipient_exists: true,
131 reject_unknown_recipients: true,
132 relay_networks: vec!["127.0.0.0/8".to_string()],
133 local_domains: vec!["localhost".to_string()],
134 connection_timeout: Duration::from_secs(3600), idle_timeout: Duration::from_secs(300), blocked_networks: Vec::new(),
137 data_tempfile_threshold: 1024 * 1024, data_spill_dir: std::env::temp_dir(),
139 }
140 }
141}
142
143#[derive(Debug, Clone)]
145struct RecipientCacheEntry {
146 exists: bool,
147 cached_at: Instant,
148}
149
150#[derive(Debug, Clone)]
157struct ScramState {
158 client_first_bare: String,
159 server_first: String,
160 nonce: String,
161 username: String,
162 stored_key: Vec<u8>,
164 server_key: Vec<u8>,
167}
168
169pub struct SmtpSession {
171 remote_addr: SocketAddr,
172 state: SmtpState,
173 transaction: SmtpTransaction,
174 config: SmtpConfig,
175 authenticated: bool,
176 #[allow(dead_code)]
177 username: Option<String>,
178 #[allow(dead_code)]
179 relaying_allowed: bool,
180 #[allow(dead_code)]
181 processor_router: Arc<MailProcessorRouter>,
182 auth_backend: Arc<dyn AuthBackend>,
183 rate_limiter: Arc<RateLimiter>,
184 storage_backend: Arc<dyn StorageBackend>,
185 recipient_cache: Arc<RwLock<HashMap<String, RecipientCacheEntry>>>,
186 cram_md5_challenge: Option<String>,
188 scram_state: Option<ScramState>,
190 ehlo_used: bool,
196 pub peer_certificates: Option<Vec<rustls::pki_types::CertificateDer<'static>>>,
204}
205
206pub struct SmtpSessionHandler {
208 session: SmtpSession,
209 stream: TcpStream,
210 #[allow(dead_code)]
212 pipelined_commands: Vec<String>,
213 #[allow(dead_code)]
215 connection_started: Instant,
216 #[allow(dead_code)]
218 last_command: Instant,
219}
220
221impl SmtpSessionHandler {
222 #[allow(clippy::too_many_arguments)]
224 pub fn new(
225 stream: TcpStream,
226 remote_addr: SocketAddr,
227 config: SmtpConfig,
228 processor_router: Arc<MailProcessorRouter>,
229 auth_backend: Arc<dyn AuthBackend>,
230 rate_limiter: Arc<RateLimiter>,
231 storage_backend: Arc<dyn StorageBackend>,
232 ) -> Self {
233 let now = Instant::now();
234 Self {
235 session: SmtpSession {
236 remote_addr,
237 state: SmtpState::Connected,
238 transaction: SmtpTransaction::new(),
239 config,
240 authenticated: false,
241 username: None,
242 relaying_allowed: false,
243 processor_router,
244 auth_backend,
245 rate_limiter,
246 storage_backend,
247 recipient_cache: Arc::new(RwLock::new(HashMap::new())),
248 cram_md5_challenge: None,
249 scram_state: None,
250 ehlo_used: false,
251 peer_certificates: None,
252 },
253 stream,
254 pipelined_commands: Vec::new(),
255 connection_started: now,
256 last_command: now,
257 }
258 }
259
260 pub async fn handle(mut self) -> anyhow::Result<()> {
262 let metrics = rusmes_metrics::global_metrics();
266 let _conn_guard = metrics.connection_guard("smtp");
267 metrics.inc_smtp_connections();
269 metrics.inc_tls_session(rusmes_metrics::tls_label::NO);
275
276 let (read_half, write_half) = tokio::io::split(self.stream);
277 let mut reader = BufReader::new(read_half);
278 let mut writer = BufWriter::new(write_half);
279
280 Self::write_response_to(
282 &mut writer,
283 SmtpResponse::service_ready(&self.session.config.hostname),
284 &self.session.remote_addr,
285 )
286 .await?;
287
288 let mut line = String::new();
289
290 loop {
291 if self.connection_started.elapsed() > self.session.config.connection_timeout {
293 tracing::info!(
294 "Connection timeout exceeded for {}",
295 self.session.remote_addr
296 );
297 Self::write_response_to(
298 &mut writer,
299 SmtpResponse::new(421, "4.4.2 Connection timeout - closing connection"),
300 &self.session.remote_addr,
301 )
302 .await?;
303 break;
304 }
305
306 line.clear();
307
308 let n = match tokio::time::timeout(
310 self.session.config.idle_timeout,
311 reader.read_line(&mut line),
312 )
313 .await
314 {
315 Ok(Ok(n)) => n,
316 Ok(Err(e)) => {
317 tracing::error!("Read error from {}: {}", self.session.remote_addr, e);
318 break;
319 }
320 Err(_) => {
321 tracing::info!(
323 peer = %self.session.remote_addr,
324 "smtp.session idle timeout, closing"
325 );
326 Self::write_response_to(
327 &mut writer,
328 SmtpResponse::new(421, "4.4.2 Connection timed out due to inactivity"),
329 &self.session.remote_addr,
330 )
331 .await?;
332 break;
333 }
334 };
335
336 if n == 0 {
337 break; }
339
340 self.last_command = Instant::now();
342
343 let line_trimmed = line.trim();
344 tracing::debug!(
345 "SMTP command from {}: {}",
346 self.session.remote_addr,
347 line_trimmed
348 );
349
350 if self.session.cram_md5_challenge.is_some() {
352 let response = match self.session.handle_cram_md5_response(line_trimmed).await {
353 Ok(resp) => resp,
354 Err(e) => {
355 tracing::error!("Error handling CRAM-MD5 response: {}", e);
356 SmtpResponse::new(535, "5.7.8 Authentication credentials invalid")
357 }
358 };
359 Self::write_response_to(&mut writer, response, &self.session.remote_addr).await?;
360 continue;
361 }
362
363 if self.session.scram_state.is_some() {
365 let response = match self.session.handle_scram_client_final(line_trimmed).await {
366 Ok(resp) => resp,
367 Err(e) => {
368 tracing::error!("Error handling SCRAM-SHA-256 client-final: {}", e);
369 self.session.scram_state = None;
370 SmtpResponse::new(535, "5.7.8 Authentication credentials invalid")
371 }
372 };
373 Self::write_response_to(&mut writer, response, &self.session.remote_addr).await?;
374 continue;
375 }
376
377 let command = match parse_command_smtputf8(line_trimmed, self.session.ehlo_used) {
380 Ok(cmd) => cmd,
381 Err(e) => {
382 tracing::warn!("Failed to parse command: {}", e);
383 Self::write_response_to(
384 &mut writer,
385 SmtpResponse::syntax_error("Command not recognized"),
386 &self.session.remote_addr,
387 )
388 .await?;
389 continue;
390 }
391 };
392
393 let response = match self.session.handle_command(command.clone()).await {
395 Ok(resp) => resp,
396 Err(e) => {
397 tracing::error!("Error handling command: {}", e);
398 rusmes_metrics::global_metrics().inc_smtp_errors();
399 SmtpResponse::local_error("Internal server error")
400 }
401 };
402
403 Self::write_response_to(&mut writer, response, &self.session.remote_addr).await?;
404
405 if self.session.state == SmtpState::Quit {
407 break;
408 }
409
410 if matches!(command, SmtpCommand::Data) && self.session.state == SmtpState::Data {
414 let remote_addr = self.session.remote_addr;
416 if let Err(e) = Self::handle_data_input(
417 &mut self.session,
418 &mut reader,
419 &mut writer,
420 &remote_addr,
421 )
422 .await
423 {
424 tracing::error!("Error reading message data: {}", e);
425 Self::write_response_to(
426 &mut writer,
427 SmtpResponse::local_error("Error reading message data"),
428 &remote_addr,
429 )
430 .await?;
431 }
432 }
433 }
434
435 Ok(())
436 }
437
438 pub(crate) async fn handle_data_input<R, W>(
444 session: &mut SmtpSession,
445 reader: &mut R,
446 writer: &mut W,
447 remote_addr: &SocketAddr,
448 ) -> anyhow::Result<()>
449 where
450 R: AsyncBufReadExt + Unpin,
451 W: AsyncWriteExt + Unpin,
452 {
453 data::handle_data_input(session, reader, writer, remote_addr).await
454 }
455
456 pub(crate) async fn write_response_to<W: AsyncWriteExt + Unpin>(
458 writer: &mut W,
459 response: SmtpResponse,
460 remote_addr: &SocketAddr,
461 ) -> anyhow::Result<()> {
462 let formatted = response.format();
463 tracing::debug!("SMTP response to {}: {}", remote_addr, formatted.trim());
464 writer.write_all(formatted.as_bytes()).await?;
465 writer.flush().await?;
466 Ok(())
467 }
468}
469
470impl SmtpSession {
471 async fn handle_command(&mut self, command: SmtpCommand) -> anyhow::Result<SmtpResponse> {
473 match command {
474 SmtpCommand::Helo(domain) => self.handle_helo(domain).await,
475 SmtpCommand::Ehlo(domain) => self.handle_ehlo(domain).await,
476 SmtpCommand::Mail { from, params } => self.handle_mail(from, params).await,
477 SmtpCommand::Rcpt { to, params } => self.handle_rcpt(to, params).await,
478 SmtpCommand::Data => self.handle_data().await,
479 SmtpCommand::Bdat { chunk_size, last } => self.handle_bdat(chunk_size, last).await,
480 SmtpCommand::Rset => self.handle_rset().await,
481 SmtpCommand::Noop => Ok(SmtpResponse::ok_simple()),
482 SmtpCommand::Quit => self.handle_quit().await,
483 SmtpCommand::StartTls => self.handle_starttls().await,
484 SmtpCommand::Auth {
485 mechanism,
486 initial_response,
487 } => self.handle_auth(mechanism, initial_response).await,
488 _ => Ok(SmtpResponse::not_implemented("Command not implemented")),
489 }
490 }
491
492 async fn handle_helo(&mut self, domain: String) -> anyhow::Result<SmtpResponse> {
494 if self.state != SmtpState::Connected && self.state != SmtpState::Authenticated {
495 return Ok(SmtpResponse::bad_sequence("Out of sequence"));
496 }
497
498 tracing::info!(
499 peer = %self.remote_addr,
500 client_hostname = %domain,
501 "smtp.session HELO received"
502 );
503 self.transaction.helo_name = Some(domain);
504 self.ehlo_used = false;
506 self.state = SmtpState::Authenticated;
507
508 Ok(SmtpResponse::ok(format!(
509 "{} Hello {}",
510 self.config.hostname,
511 self.remote_addr.ip()
512 )))
513 }
514
515 async fn handle_ehlo(&mut self, domain: String) -> anyhow::Result<SmtpResponse> {
517 if self.state != SmtpState::Connected && self.state != SmtpState::Authenticated {
518 return Ok(SmtpResponse::bad_sequence("Out of sequence"));
519 }
520
521 tracing::info!(
522 peer = %self.remote_addr,
523 client_hostname = %domain,
524 "smtp.session EHLO received"
525 );
526 self.transaction.helo_name = Some(domain);
527 self.ehlo_used = true;
529 self.state = SmtpState::Authenticated;
530
531 let mut extensions = vec![
532 format!("SIZE {}", self.config.max_message_size),
533 "8BITMIME".to_string(),
534 "SMTPUTF8".to_string(),
535 "PIPELINING".to_string(),
536 "CHUNKING".to_string(), ];
538
539 if self.config.enable_starttls {
540 extensions.push("STARTTLS".to_string());
541 }
542
543 if self.config.require_auth {
544 extensions.push("AUTH PLAIN LOGIN CRAM-MD5 SCRAM-SHA-256".to_string());
545 }
546
547 Ok(SmtpResponse::ehlo(&self.config.hostname, extensions))
548 }
549
550 async fn handle_mail(
552 &mut self,
553 from: MailAddress,
554 params: Vec<crate::command::MailParam>,
555 ) -> anyhow::Result<SmtpResponse> {
556 if self.state != SmtpState::Authenticated {
557 return Ok(SmtpResponse::bad_sequence("Must send HELO/EHLO first"));
558 }
559
560 if self.config.require_auth && !self.authenticated {
561 return Ok(SmtpResponse::bad_sequence("Authentication required"));
562 }
563
564 let ip = self.remote_addr.ip();
566 if !self
567 .rate_limiter
568 .allow_message_ip_and_sender(ip, &from.as_string())
569 .await
570 {
571 tracing::warn!("Message rate limit exceeded for {} from {}", from, ip);
572 return Ok(SmtpResponse::mailbox_unavailable(
573 "Rate limit exceeded, please try again later",
574 ));
575 }
576
577 for param in params {
579 match param.keyword.to_uppercase().as_str() {
580 "SIZE" => {
581 if let Some(size_str) = param.value {
583 match size_str.parse::<usize>() {
584 Ok(size) => {
585 if size > self.config.max_message_size {
586 return Ok(SmtpResponse::storage_exceeded(format!(
587 "Message size {} exceeds maximum {}",
588 size, self.config.max_message_size
589 )));
590 }
591 self.transaction.declared_size = Some(size);
592 }
593 Err(_) => {
594 return Ok(SmtpResponse::parameter_error("Invalid SIZE parameter"));
595 }
596 }
597 } else {
598 return Ok(SmtpResponse::parameter_error(
599 "SIZE parameter requires a value",
600 ));
601 }
602 }
603 "BODY" => {
604 if let Some(body_value) = param.value {
606 let body_upper = body_value.to_uppercase();
607 match body_upper.as_str() {
608 "7BIT" | "8BITMIME" => {
609 self.transaction.body_type = Some(body_upper);
610 }
611 _ => {
612 return Ok(SmtpResponse::parameter_not_implemented(format!(
613 "Unsupported BODY type: {}",
614 body_value
615 )));
616 }
617 }
618 } else {
619 return Ok(SmtpResponse::parameter_error(
620 "BODY parameter requires a value",
621 ));
622 }
623 }
624 "SMTPUTF8" => {
625 if !self.ehlo_used {
629 return Ok(SmtpResponse::parameter_error(
630 "SMTPUTF8 requires EHLO (not HELO)",
631 ));
632 }
633 if param.value.is_none() {
634 self.transaction.smtputf8 = true;
635 } else {
636 return Ok(SmtpResponse::parameter_error(
637 "SMTPUTF8 parameter must not have a value",
638 ));
639 }
640 }
641 _ => {
642 tracing::debug!("Unknown MAIL parameter: {}", param.keyword);
644 }
645 }
646 }
647
648 if from.local_part().bytes().any(|b| b >= 0x80) && !self.transaction.smtputf8 {
652 return Ok(SmtpResponse::new(
653 501,
654 "5.5.4 Non-ASCII local-part requires SMTPUTF8 parameter (RFC 6531 §3.4)",
655 ));
656 }
657
658 tracing::info!(
659 peer = %self.remote_addr,
660 mail_from = %from,
661 "smtp.session MAIL FROM accepted"
662 );
663 self.transaction.sender = Some(from.clone());
664 self.state = SmtpState::MailTransaction;
665
666 Ok(SmtpResponse::ok(format!("Sender {} OK", from)))
667 }
668
669 async fn handle_rcpt(
671 &mut self,
672 to: MailAddress,
673 params: Vec<crate::command::MailParam>,
674 ) -> anyhow::Result<SmtpResponse> {
675 if self.state != SmtpState::MailTransaction {
676 return Ok(SmtpResponse::bad_sequence("Must send MAIL FROM first"));
677 }
678
679 for param in params {
681 tracing::debug!("Unknown RCPT parameter: {}", param.keyword);
683 }
684
685 if !self.is_relay_allowed(&to) {
687 tracing::info!(
688 peer = %self.remote_addr,
689 rcpt_to = %to,
690 "smtp.session RCPT TO rejected: relaying denied"
691 );
692 return Ok(SmtpResponse::new(550, "5.7.1 Relaying denied"));
693 }
694
695 if self.config.check_recipient_exists {
697 if !self.authenticated && !self.relaying_allowed {
699 match self.validate_recipient(&to).await {
700 Ok(true) => {
701 }
703 Ok(false) => {
704 if self.config.reject_unknown_recipients {
705 tracing::warn!("Rejecting unknown recipient: {}", to);
706 return Ok(SmtpResponse::new(
707 550,
708 format!("5.1.1 User unknown: {}", to),
709 ));
710 } else {
711 tracing::info!(
713 "Accepting unknown recipient (rejection disabled): {}",
714 to
715 );
716 }
717 }
718 Err(e) => {
719 tracing::error!("Error validating recipient {}: {}", to, e);
720 tracing::warn!("Accepting recipient {} due to validation error", to);
722 }
723 }
724 }
725 }
726
727 tracing::info!(
729 peer = %self.remote_addr,
730 rcpt_to = %to,
731 "smtp.session RCPT TO accepted"
732 );
733 self.transaction.recipients.push(to.clone());
734
735 Ok(SmtpResponse::ok(format!("Recipient {} OK", to)))
736 }
737
738 async fn handle_data(&mut self) -> anyhow::Result<SmtpResponse> {
740 if self.state != SmtpState::MailTransaction {
741 return Ok(SmtpResponse::bad_sequence("Must send RCPT TO first"));
742 }
743
744 if !self.transaction.is_valid() {
745 return Ok(SmtpResponse::bad_sequence("Need at least one recipient"));
746 }
747
748 self.state = SmtpState::Data;
749 Ok(SmtpResponse::start_data())
750 }
751
752 async fn handle_bdat(&mut self, chunk_size: usize, last: bool) -> anyhow::Result<SmtpResponse> {
757 if self.state != SmtpState::MailTransaction {
759 return Ok(SmtpResponse::bad_sequence(
760 "Must send MAIL FROM and RCPT TO first",
761 ));
762 }
763
764 if !self.transaction.is_valid() {
765 return Ok(SmtpResponse::bad_sequence(
766 "Need sender and at least one recipient",
767 ));
768 }
769
770 if self.transaction.bdat_state.is_none() {
772 self.transaction.bdat_state = Some(crate::BdatState::new(self.config.max_message_size));
773 }
774
775 let bdat_state = match self.transaction.bdat_state.as_ref() {
781 Some(state) => state,
782 None => {
783 return Err(anyhow::anyhow!(
784 "Internal error: bdat_state not initialized"
785 ))
786 }
787 };
788 if bdat_state.total_size() + chunk_size > self.config.max_message_size {
789 return Ok(SmtpResponse::storage_exceeded(format!(
790 "Message size {} exceeds maximum {}",
791 bdat_state.total_size() + chunk_size,
792 self.config.max_message_size
793 )));
794 }
795
796 if last {
798 let sender_display = self
799 .transaction
800 .sender
801 .as_ref()
802 .map(|a| a.to_string())
803 .unwrap_or_else(|| "<unknown>".to_string());
804 tracing::info!(
805 "BDAT LAST chunk ({} bytes) from {} with {} recipient(s)",
806 chunk_size,
807 sender_display,
808 self.transaction.recipients.len()
809 );
810 }
811
812 Ok(SmtpResponse::ok(format!("{} octets received", chunk_size)))
814 }
815
816 async fn handle_rset(&mut self) -> anyhow::Result<SmtpResponse> {
818 self.transaction.reset();
819 self.state = SmtpState::Authenticated;
820 Ok(SmtpResponse::ok_simple())
821 }
822
823 async fn handle_quit(&mut self) -> anyhow::Result<SmtpResponse> {
825 tracing::info!(
826 peer = %self.remote_addr,
827 "smtp.session QUIT received"
828 );
829 self.state = SmtpState::Quit;
830 Ok(SmtpResponse::closing())
831 }
832
833 async fn handle_starttls(&mut self) -> anyhow::Result<SmtpResponse> {
835 if !self.config.enable_starttls {
836 return Ok(SmtpResponse::not_implemented("STARTTLS not available"));
837 }
838
839 rusmes_metrics::global_metrics().inc_tls_session(rusmes_metrics::tls_label::STARTTLS);
845
846 Ok(SmtpResponse::new(220, "Ready to start TLS"))
847 }
848
849 async fn handle_auth(
851 &mut self,
852 mechanism: String,
853 initial_response: Option<String>,
854 ) -> anyhow::Result<SmtpResponse> {
855 if !self.config.require_auth {
856 return Ok(SmtpResponse::not_implemented("AUTH not available"));
857 }
858
859 match mechanism.to_uppercase().as_str() {
860 "CRAM-MD5" => self.handle_auth_cram_md5().await,
861 "SCRAM-SHA-256" => self.handle_auth_scram_sha256(initial_response).await,
862 "PLAIN" => {
863 if let Some(response) = initial_response {
864 self.handle_auth_plain(response).await
865 } else {
866 Ok(SmtpResponse::new(334, ""))
868 }
869 }
870 "LOGIN" => {
871 Ok(SmtpResponse::new(334, "VXNlcm5hbWU6"))
874 }
875 _ => Ok(SmtpResponse::parameter_not_implemented(
876 "Authentication mechanism not supported",
877 )),
878 }
879 }
880
881 async fn handle_auth_plain(&mut self, response: String) -> anyhow::Result<SmtpResponse> {
883 let (username, password) = match crate::auth::parse_plain_auth(&response) {
885 Ok(creds) => creds,
886 Err(e) => {
887 tracing::warn!("Failed to parse PLAIN auth: {}", e);
888 rusmes_metrics::global_metrics().inc_smtp_auth_failure();
889 return Ok(SmtpResponse::new(535, "5.7.8 Authentication failed"));
890 }
891 };
892
893 let username_obj = match rusmes_proto::Username::new(username.clone()) {
895 Ok(u) => u,
896 Err(e) => {
897 tracing::warn!("Invalid username '{}': {}", username, e);
898 rusmes_metrics::global_metrics().inc_smtp_auth_failure();
899 return Ok(SmtpResponse::new(535, "5.7.8 Authentication failed"));
900 }
901 };
902
903 match self
905 .auth_backend
906 .authenticate(&username_obj, &password)
907 .await
908 {
909 Ok(true) => {
910 self.authenticated = true;
911 self.username = Some(username.clone());
912 tracing::info!(
913 peer = %self.remote_addr,
914 username = %username,
915 mechanism = "PLAIN",
916 "smtp.session AUTH success"
917 );
918 rusmes_metrics::global_metrics().inc_smtp_auth_success();
919 Ok(SmtpResponse::new(235, "2.7.0 Authentication successful"))
920 }
921 Ok(false) => {
922 tracing::warn!(
923 peer = %self.remote_addr,
924 username = %username,
925 mechanism = "PLAIN",
926 "smtp.session AUTH failure"
927 );
928 rusmes_metrics::global_metrics().inc_smtp_auth_failure();
929 Ok(SmtpResponse::new(535, "5.7.8 Authentication failed"))
930 }
931 Err(e) => {
932 tracing::error!(
933 peer = %self.remote_addr,
934 username = %username,
935 error = %e,
936 "smtp.session AUTH backend error"
937 );
938 rusmes_metrics::global_metrics().inc_smtp_auth_failure();
939 Ok(SmtpResponse::new(535, "5.7.8 Authentication failed"))
940 }
941 }
942 }
943
944 async fn handle_auth_cram_md5(&mut self) -> anyhow::Result<SmtpResponse> {
946 let challenge = crate::auth::generate_cram_md5_challenge(&self.config.hostname)?;
948
949 self.cram_md5_challenge = Some(challenge.clone());
951
952 let encoded = crate::auth::encode_challenge(&challenge);
954 Ok(SmtpResponse::new(334, encoded))
955 }
956
957 async fn handle_cram_md5_response(
959 &mut self,
960 response_line: &str,
961 ) -> anyhow::Result<SmtpResponse> {
962 let challenge = self
964 .cram_md5_challenge
965 .take()
966 .ok_or_else(|| anyhow::anyhow!("No CRAM-MD5 challenge pending"))?;
967
968 if response_line == "*" {
970 tracing::info!("CRAM-MD5 authentication aborted by client");
971 return Ok(SmtpResponse::new(501, "5.7.0 Authentication aborted"));
972 }
973
974 let decoded = crate::auth::decode_response(response_line)?;
976
977 let (username, client_hmac) = crate::auth::parse_cram_md5_response(&decoded)?;
979
980 tracing::warn!(
991 "CRAM-MD5 authentication attempted for user '{}', but cannot verify HMAC with bcrypt-hashed passwords",
992 username
993 );
994
995 tracing::info!(
1001 "CRAM-MD5 authentication for user '{}' from {} - challenge: {}, client_hmac: {}",
1002 username,
1003 self.remote_addr,
1004 challenge,
1005 client_hmac
1006 );
1007
1008 let username_obj = rusmes_proto::Username::new(username.to_string())
1010 .map_err(|e| anyhow::anyhow!("Invalid username: {}", e))?;
1011
1012 let user_exists = self.auth_backend.verify_identity(&username_obj).await?;
1013
1014 if !user_exists {
1015 tracing::warn!(
1016 "CRAM-MD5 authentication failed: user '{}' does not exist",
1017 username
1018 );
1019 rusmes_metrics::global_metrics().inc_smtp_auth_failure();
1020 return Ok(SmtpResponse::new(
1021 535,
1022 "5.7.8 Authentication credentials invalid",
1023 ));
1024 }
1025
1026 tracing::warn!(
1034 "CRAM-MD5 authentication rejected: mechanism requires plaintext password storage"
1035 );
1036
1037 rusmes_metrics::global_metrics().inc_smtp_auth_failure();
1038 Ok(SmtpResponse::new(
1039 535,
1040 "5.7.8 Authentication credentials invalid",
1041 ))
1042 }
1043
1044 fn is_relay_allowed(&self, recipient: &MailAddress) -> bool {
1051 if self.authenticated {
1053 tracing::debug!(
1054 "Relay allowed for {} from {}: authenticated user",
1055 recipient,
1056 self.remote_addr.ip()
1057 );
1058 return true;
1059 }
1060
1061 if crate::is_ip_in_networks(self.remote_addr.ip(), &self.config.relay_networks) {
1063 tracing::debug!(
1064 "Relay allowed for {} from {}: client IP in relay_networks",
1065 recipient,
1066 self.remote_addr.ip()
1067 );
1068 return true;
1069 }
1070
1071 let recipient_domain = recipient.domain().as_str();
1073 for local_domain in &self.config.local_domains {
1074 if recipient_domain.eq_ignore_ascii_case(local_domain) {
1075 tracing::debug!(
1076 "Relay allowed for {} from {}: local domain",
1077 recipient,
1078 self.remote_addr.ip()
1079 );
1080 return true;
1081 }
1082 }
1083
1084 tracing::warn!(
1086 "Relay denied for {} from {}: not authenticated, not in relay_networks, not local domain",
1087 recipient,
1088 self.remote_addr.ip()
1089 );
1090 false
1091 }
1092
1093 async fn validate_recipient(&self, recipient: &MailAddress) -> anyhow::Result<bool> {
1095 const CACHE_TTL: Duration = Duration::from_secs(300);
1097
1098 let cache_key = recipient.as_string();
1099
1100 {
1102 let cache = self.recipient_cache.read().await;
1103 if let Some(entry) = cache.get(&cache_key) {
1104 if entry.cached_at.elapsed() < CACHE_TTL {
1105 tracing::debug!("Recipient validation cache hit for {}", recipient);
1106 return Ok(entry.exists);
1107 }
1108 }
1109 }
1110
1111 tracing::debug!(
1113 "Recipient validation cache miss for {}, querying storage",
1114 recipient
1115 );
1116
1117 let username = Username::new(recipient.local_part())?;
1119
1120 let mailbox_store = self.storage_backend.mailbox_store();
1122 let mailboxes = mailbox_store.list_mailboxes(&username).await?;
1123
1124 let exists = !mailboxes.is_empty();
1125
1126 {
1128 let mut cache = self.recipient_cache.write().await;
1129 cache.insert(
1130 cache_key,
1131 RecipientCacheEntry {
1132 exists,
1133 cached_at: Instant::now(),
1134 },
1135 );
1136 }
1137
1138 Ok(exists)
1139 }
1140
1141 async fn handle_auth_scram_sha256(
1143 &mut self,
1144 initial_response: Option<String>,
1145 ) -> anyhow::Result<SmtpResponse> {
1146 let client_first_encoded = match initial_response {
1149 Some(resp) => resp,
1150 None => {
1151 return Ok(SmtpResponse::new(334, ""));
1153 }
1154 };
1155
1156 let client_first_decoded = BASE64
1158 .decode(client_first_encoded.trim())
1159 .map_err(|e| anyhow::anyhow!("Failed to decode client-first: {}", e))?;
1160 let client_first_str = String::from_utf8(client_first_decoded)
1161 .map_err(|e| anyhow::anyhow!("Failed to decode UTF-8: {}", e))?;
1162
1163 let (username, client_nonce, client_first_bare) =
1165 crate::auth::parse_scram_client_first(&client_first_str)?;
1166
1167 let server_nonce = crate::auth::generate_scram_server_nonce()?;
1169 let nonce = format!("{}{}", client_nonce, server_nonce);
1170
1171 let creds = match self.auth_backend.fetch_scram_credentials(&username).await {
1179 Ok(Some(creds)) => creds,
1180 Ok(None) => {
1181 tracing::info!(
1182 "SCRAM-SHA-256 declined for user '{}': no SCRAM credentials stored",
1183 username
1184 );
1185 rusmes_metrics::global_metrics().inc_smtp_auth_failure();
1188 return Ok(SmtpResponse::new(
1189 504,
1190 "5.5.4 SCRAM-SHA-256 mechanism not available for this user",
1191 ));
1192 }
1193 Err(e) => {
1194 tracing::error!(
1195 "SCRAM-SHA-256 credential lookup failed for user '{}': {}",
1196 username,
1197 e
1198 );
1199 rusmes_metrics::global_metrics().inc_smtp_auth_failure();
1200 return Ok(SmtpResponse::new(
1201 454,
1202 "4.7.0 Temporary authentication failure",
1203 ));
1204 }
1205 };
1206
1207 let server_first = format!(
1209 "r={},s={},i={}",
1210 nonce,
1211 BASE64.encode(&creds.salt),
1212 creds.iteration_count
1213 );
1214
1215 self.scram_state = Some(ScramState {
1217 client_first_bare: client_first_bare.clone(),
1218 server_first: server_first.clone(),
1219 nonce: nonce.clone(),
1220 username: username.clone(),
1221 stored_key: creds.stored_key,
1222 server_key: creds.server_key,
1223 });
1224
1225 let server_first_encoded = BASE64.encode(server_first.as_bytes());
1227 Ok(SmtpResponse::new(334, server_first_encoded))
1228 }
1229
1230 async fn handle_scram_client_final(
1232 &mut self,
1233 client_final_line: &str,
1234 ) -> anyhow::Result<SmtpResponse> {
1235 let state = self
1237 .scram_state
1238 .take()
1239 .ok_or_else(|| anyhow::anyhow!("No SCRAM state"))?;
1240
1241 if client_final_line == "*" {
1243 tracing::info!("SCRAM-SHA-256 authentication aborted by client");
1244 return Ok(SmtpResponse::new(501, "5.7.0 Authentication aborted"));
1245 }
1246
1247 let client_final_decoded = BASE64
1249 .decode(client_final_line.trim())
1250 .map_err(|e| anyhow::anyhow!("Failed to decode client-final: {}", e))?;
1251 let client_final_str = String::from_utf8(client_final_decoded)
1252 .map_err(|e| anyhow::anyhow!("Failed to decode UTF-8: {}", e))?;
1253
1254 let (_channel_binding, nonce, proof, client_final_without_proof) =
1256 crate::auth::parse_scram_client_final(&client_final_str)?;
1257
1258 if nonce != state.nonce {
1260 tracing::warn!(
1261 "SCRAM-SHA-256 nonce mismatch for user '{}': expected '{}', got '{}'",
1262 state.username,
1263 state.nonce,
1264 nonce
1265 );
1266 rusmes_metrics::global_metrics().inc_smtp_auth_failure();
1267 return Ok(SmtpResponse::new(
1268 535,
1269 "5.7.8 Authentication credentials invalid",
1270 ));
1271 }
1272
1273 let auth_message = format!(
1276 "{},{},{}",
1277 state.client_first_bare, state.server_first, client_final_without_proof
1278 );
1279
1280 let proof_valid =
1282 crate::auth::verify_scram_client_proof(&state.stored_key, &auth_message, &proof)?;
1283
1284 if !proof_valid {
1285 tracing::warn!(
1286 "SCRAM-SHA-256 authentication failed for user '{}': client proof did not verify",
1287 state.username
1288 );
1289 rusmes_metrics::global_metrics().inc_smtp_auth_failure();
1290 return Ok(SmtpResponse::new(
1291 535,
1292 "5.7.8 Authentication credentials invalid",
1293 ));
1294 }
1295
1296 let server_signature =
1299 crate::auth::compute_scram_server_signature(&state.server_key, &auth_message)?;
1300 let server_final = format!("v={}", server_signature);
1301 let server_final_b64 = BASE64.encode(server_final.as_bytes());
1302
1303 self.authenticated = true;
1305 self.username = Some(state.username.clone());
1306
1307 tracing::info!(
1308 "User '{}' authenticated successfully (SCRAM-SHA-256)",
1309 state.username
1310 );
1311
1312 rusmes_metrics::global_metrics().inc_smtp_auth_success();
1313 Ok(SmtpResponse::new(
1314 235,
1315 format!("2.7.0 {} Authentication successful", server_final_b64),
1316 ))
1317 }
1318}
1319
1320#[cfg(test)]
1321mod tests;