1use crate::{SmtpConfig, SmtpSpecRegistry};
4use mockforge_core::protocol_abstraction::{
5 MessagePattern, MiddlewareChain, Protocol, ProtocolRequest, SpecRegistry,
6};
7use mockforge_core::Result;
8use std::collections::HashMap;
9use std::net::SocketAddr;
10use std::sync::Arc;
11use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
12use tokio::net::{TcpListener, TcpStream};
13use tokio_rustls::{rustls, TlsAcceptor};
14use tracing::{debug, error, info, warn};
15
16pub struct SmtpServer {
18 config: SmtpConfig,
19 spec_registry: Arc<SmtpSpecRegistry>,
20 middleware_chain: Arc<MiddlewareChain>,
21 #[allow(dead_code)]
22 tls_acceptor: Option<TlsAcceptor>,
23}
24
25impl SmtpServer {
26 pub fn new(config: SmtpConfig, spec_registry: Arc<SmtpSpecRegistry>) -> Result<Self> {
28 let middleware_chain = Arc::new(MiddlewareChain::new());
29
30 let tls_acceptor = if config.enable_starttls {
31 Some(Self::load_tls_acceptor(&config)?)
32 } else {
33 None
34 };
35
36 Ok(Self {
37 config,
38 spec_registry,
39 middleware_chain,
40 tls_acceptor,
41 })
42 }
43
44 fn load_tls_acceptor(config: &SmtpConfig) -> Result<TlsAcceptor> {
46 use rustls_pemfile::{certs, pkcs8_private_keys};
47 use std::fs::File;
48 use std::io::BufReader;
49
50 let cert_path = config.tls_cert_path.as_ref().ok_or_else(|| {
51 mockforge_core::Error::internal("TLS certificate path not configured")
52 })?;
53 let key_path = config.tls_key_path.as_ref().ok_or_else(|| {
54 mockforge_core::Error::internal("TLS private key path not configured")
55 })?;
56
57 let cert_file = File::open(cert_path)?;
59 let mut cert_reader = BufReader::new(cert_file);
60 let certs: Vec<Vec<u8>> = certs(&mut cert_reader)?;
61 let certs: Vec<rustls::Certificate> = certs.into_iter().map(rustls::Certificate).collect();
63
64 let key_file = File::open(key_path)?;
66 let mut key_reader = BufReader::new(key_file);
67 let mut keys: Vec<Vec<u8>> = pkcs8_private_keys(&mut key_reader)?;
68
69 if keys.is_empty() {
70 return Err(mockforge_core::Error::internal("No private keys found"));
71 }
72
73 let mut server_config = rustls::ServerConfig::builder()
75 .with_safe_defaults()
76 .with_no_client_auth()
77 .with_single_cert(certs, rustls::PrivateKey(keys.remove(0)))
78 .map_err(|e| mockforge_core::Error::internal(format!("TLS config error: {}", e)))?;
79
80 server_config.alpn_protocols = vec![b"smtp".to_vec()];
81
82 Ok(TlsAcceptor::from(Arc::new(server_config)))
83 }
84
85 pub fn with_middleware(
87 config: SmtpConfig,
88 spec_registry: Arc<SmtpSpecRegistry>,
89 middleware_chain: Arc<MiddlewareChain>,
90 ) -> Result<Self> {
91 let tls_acceptor = if config.enable_starttls {
92 Some(Self::load_tls_acceptor(&config)?)
93 } else {
94 None
95 };
96
97 Ok(Self {
98 config,
99 spec_registry,
100 middleware_chain,
101 tls_acceptor,
102 })
103 }
104
105 pub async fn start(&self) -> Result<()> {
107 let addr = format!("{}:{}", self.config.host, self.config.port);
108 let listener = TcpListener::bind(&addr).await?;
109
110 info!("SMTP server listening on {}", addr);
111
112 loop {
113 match listener.accept().await {
114 Ok((stream, peer_addr)) => {
115 debug!("New SMTP connection from {}", peer_addr);
116
117 let registry = self.spec_registry.clone();
118 let middleware = self.middleware_chain.clone();
119 let hostname = self.config.hostname.clone();
120
121 tokio::spawn(async move {
122 if let Err(e) =
123 handle_smtp_session(stream, peer_addr, registry, middleware, hostname)
124 .await
125 {
126 error!("SMTP session error from {}: {}", peer_addr, e);
127 }
128 });
129 }
130 Err(e) => {
131 error!("Failed to accept SMTP connection: {}", e);
132 }
133 }
134 }
135 }
136}
137
138async fn handle_smtp_session(
140 stream: TcpStream,
141 peer_addr: SocketAddr,
142 registry: Arc<SmtpSpecRegistry>,
143 middleware: Arc<MiddlewareChain>,
144 hostname: String,
145) -> Result<()> {
146 let (reader, mut writer) = stream.into_split();
147 let mut reader = BufReader::new(reader);
148
149 let greeting = format!("220 {} ESMTP MockForge SMTP Server\r\n", hostname);
151 writer.write_all(greeting.as_bytes()).await?;
152
153 let mut session_state = SessionState::new();
154 let mut line = String::new();
155
156 while reader.read_line(&mut line).await? > 0 {
157 let command = line.trim();
158 debug!("SMTP command from {}: {}", peer_addr, command);
159
160 if command.is_empty() {
161 line.clear();
162 continue;
163 }
164
165 match handle_smtp_command(
167 command,
168 &mut session_state,
169 &mut writer,
170 &hostname,
171 ®istry,
172 &middleware,
173 peer_addr,
174 )
175 .await
176 {
177 Ok(should_continue) => {
178 if !should_continue {
179 debug!("SMTP session ended for {}", peer_addr);
180 break;
181 }
182 }
183 Err(e) => {
184 error!("Error handling SMTP command: {}", e);
185 let error_response = "500 Internal server error\r\n";
186 writer.write_all(error_response.as_bytes()).await?;
187 }
188 }
189
190 line.clear();
191 }
192
193 Ok(())
194}
195
196async fn handle_smtp_command<W: AsyncWriteExt + Unpin>(
198 command: &str,
199 state: &mut SessionState,
200 writer: &mut W,
201 hostname: &str,
202 registry: &Arc<SmtpSpecRegistry>,
203 middleware: &Arc<MiddlewareChain>,
204 peer_addr: SocketAddr,
205) -> Result<bool> {
206 let parts: Vec<&str> = command.splitn(2, ' ').collect();
207 let cmd = parts[0].to_uppercase();
208
209 match cmd.as_str() {
210 "HELLO" | "EHLO" => {
211 let domain = parts.get(1).unwrap_or(&hostname);
212 let response = if cmd == "EHLO" {
213 format!(
214 "250-{} Hello {}\r\n250-SIZE 10485760\r\n250-8BITMIME\r\n250-STARTTLS\r\n250 HELP\r\n",
215 hostname, domain
216 )
217 } else {
218 format!("250 {} Hello {}\r\n", hostname, domain)
219 };
220 writer.write_all(response.as_bytes()).await?;
221 Ok(true)
222 }
223
224 "MAIL" => {
225 if let Some(from_part) = parts.get(1) {
226 let from = extract_email_address(from_part);
228 state.mail_from = Some(from);
229 writer.write_all(b"250 OK\r\n").await?;
230 } else {
231 writer.write_all(b"501 Syntax error in parameters\r\n").await?;
232 }
233 Ok(true)
234 }
235
236 "RCPT" => {
237 if let Some(to_part) = parts.get(1) {
238 let to = extract_email_address(to_part);
240 state.rcpt_to.push(to);
241 writer.write_all(b"250 OK\r\n").await?;
242 } else {
243 writer.write_all(b"501 Syntax error in parameters\r\n").await?;
244 }
245 Ok(true)
246 }
247
248 "DATA" => {
249 writer.write_all(b"354 Start mail input; end with <CRLF>.<CRLF>\r\n").await?;
250 state.in_data_mode = true;
251 Ok(true)
252 }
253
254 "RSET" => {
255 state.reset();
256 writer.write_all(b"250 OK\r\n").await?;
257 Ok(true)
258 }
259
260 "NOOP" => {
261 writer.write_all(b"250 OK\r\n").await?;
262 Ok(true)
263 }
264
265 "QUIT" => {
266 writer.write_all(b"221 Bye\r\n").await?;
267 Ok(false) }
269
270 "STARTTLS" => {
271 writer.write_all(b"220 Ready to start TLS\r\n").await?;
273 Ok(true)
274 }
275
276 "HELP" => {
277 let help_text = "214-Commands supported:\r\n\
278 214- HELLO EHLO MAIL RCPT DATA\r\n\
279 214- RSET NOOP QUIT HELP STARTTLS\r\n\
280 214 End of HELP info\r\n";
281 writer.write_all(help_text.as_bytes()).await?;
282 Ok(true)
283 }
284
285 _ => {
286 if state.in_data_mode {
288 if command == "." {
289 state.in_data_mode = false;
291
292 let response = process_email(state, registry, middleware, peer_addr).await?;
294
295 writer.write_all(response.as_bytes()).await?;
296 state.reset();
297 } else {
298 state.data.push_str(command);
300 state.data.push('\n');
301 }
302 Ok(true)
303 } else {
304 warn!("Unknown SMTP command: {}", command);
305 writer.write_all(b"502 Command not implemented\r\n").await?;
306 Ok(true)
307 }
308 }
309 }
310}
311
312async fn process_email(
314 state: &SessionState,
315 registry: &Arc<SmtpSpecRegistry>,
316 middleware: &Arc<MiddlewareChain>,
317 peer_addr: SocketAddr,
318) -> Result<String> {
319 let from = state
320 .mail_from
321 .as_ref()
322 .ok_or_else(|| mockforge_core::Error::internal("Missing MAIL FROM"))?;
323 let to = state.rcpt_to.join(", ");
324
325 let subject = extract_subject(&state.data);
327
328 let mut request = ProtocolRequest {
330 protocol: Protocol::Smtp,
331 pattern: MessagePattern::OneWay,
332 operation: "SEND".to_string(),
333 path: from.clone(),
334 topic: None,
335 routing_key: None,
336 partition: None,
337 qos: None,
338 metadata: HashMap::from([
339 ("from".to_string(), from.clone()),
340 ("to".to_string(), to.clone()),
341 ("subject".to_string(), subject.clone()),
342 ]),
343 body: Some(state.data.as_bytes().to_vec()),
344 client_ip: Some(peer_addr.ip().to_string()),
345 };
346
347 if let Some(short_circuit_response) = middleware.process_request(&mut request).await? {
349 return Ok(String::from_utf8_lossy(&short_circuit_response.body).to_string());
350 }
351
352 let mut response = registry.generate_mock_response(&request)?;
354
355 middleware.process_response(&request, &mut response).await?;
357
358 Ok(String::from_utf8_lossy(&response.body).to_string())
360}
361
362fn extract_email_address(param: &str) -> String {
364 if let Some(start) = param.find('<') {
366 if let Some(end) = param.find('>') {
367 return param[start + 1..end].to_string();
368 }
369 }
370
371 param.trim().to_string()
373}
374
375fn extract_subject(data: &str) -> String {
377 for line in data.lines() {
378 if line.to_lowercase().starts_with("subject:") {
379 return line[8..].trim().to_string();
380 }
381 }
382 String::new()
383}
384
385struct SessionState {
387 mail_from: Option<String>,
388 rcpt_to: Vec<String>,
389 data: String,
390 in_data_mode: bool,
391}
392
393impl SessionState {
394 fn new() -> Self {
395 Self {
396 mail_from: None,
397 rcpt_to: Vec::new(),
398 data: String::new(),
399 in_data_mode: false,
400 }
401 }
402
403 fn reset(&mut self) {
404 self.mail_from = None;
405 self.rcpt_to.clear();
406 self.data.clear();
407 self.in_data_mode = false;
408 }
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414
415 #[test]
416 fn test_extract_email_address() {
417 assert_eq!(extract_email_address("FROM:<user@example.com>"), "user@example.com");
418 assert_eq!(extract_email_address("TO:<admin@test.com>"), "admin@test.com");
419 assert_eq!(extract_email_address("user@example.com"), "user@example.com");
420 }
421
422 #[test]
423 fn test_extract_email_address_whitespace() {
424 assert_eq!(extract_email_address(" user@example.com "), "user@example.com");
425 }
426
427 #[test]
428 fn test_extract_email_address_no_brackets() {
429 assert_eq!(extract_email_address("plain@email.com"), "plain@email.com");
430 }
431
432 #[test]
433 fn test_extract_email_address_mail_from_format() {
434 assert_eq!(extract_email_address("FROM:<sender@domain.com>"), "sender@domain.com");
435 }
436
437 #[test]
438 fn test_extract_subject() {
439 let data =
440 "From: sender@example.com\nSubject: Test Email\nTo: recipient@example.com\n\nBody text";
441 assert_eq!(extract_subject(data), "Test Email");
442 }
443
444 #[test]
445 fn test_extract_subject_not_found() {
446 let data = "From: sender@example.com\nTo: recipient@example.com\n\nBody text";
447 assert_eq!(extract_subject(data), "");
448 }
449
450 #[test]
451 fn test_extract_subject_lowercase() {
452 let data = "subject: lowercase subject\nFrom: sender@example.com";
453 assert_eq!(extract_subject(data), "lowercase subject");
454 }
455
456 #[test]
457 fn test_extract_subject_mixed_case() {
458 let data = "SUBJECT: UPPERCASE SUBJECT\nFrom: sender@example.com";
459 assert_eq!(extract_subject(data), "UPPERCASE SUBJECT");
460 }
461
462 #[test]
463 fn test_session_state() {
464 let mut state = SessionState::new();
465 assert!(state.mail_from.is_none());
466 assert_eq!(state.rcpt_to.len(), 0);
467
468 state.mail_from = Some("sender@example.com".to_string());
469 state.rcpt_to.push("recipient@example.com".to_string());
470
471 state.reset();
472 assert!(state.mail_from.is_none());
473 assert_eq!(state.rcpt_to.len(), 0);
474 }
475
476 #[test]
477 fn test_session_state_new() {
478 let state = SessionState::new();
479 assert!(state.mail_from.is_none());
480 assert!(state.rcpt_to.is_empty());
481 assert!(state.data.is_empty());
482 assert!(!state.in_data_mode);
483 }
484
485 #[test]
486 fn test_session_state_reset() {
487 let mut state = SessionState::new();
488 state.mail_from = Some("test@example.com".to_string());
489 state.rcpt_to.push("recipient1@example.com".to_string());
490 state.rcpt_to.push("recipient2@example.com".to_string());
491 state.data = "Email body content".to_string();
492 state.in_data_mode = true;
493
494 state.reset();
495
496 assert!(state.mail_from.is_none());
497 assert!(state.rcpt_to.is_empty());
498 assert!(state.data.is_empty());
499 assert!(!state.in_data_mode);
500 }
501
502 #[test]
503 fn test_session_state_multiple_recipients() {
504 let mut state = SessionState::new();
505 state.rcpt_to.push("a@example.com".to_string());
506 state.rcpt_to.push("b@example.com".to_string());
507 state.rcpt_to.push("c@example.com".to_string());
508 assert_eq!(state.rcpt_to.len(), 3);
509 }
510
511 #[test]
512 fn test_session_state_data_accumulation() {
513 let mut state = SessionState::new();
514 state.data.push_str("Line 1\n");
515 state.data.push_str("Line 2\n");
516 state.data.push_str("Line 3\n");
517 assert_eq!(state.data, "Line 1\nLine 2\nLine 3\n");
518 }
519
520 #[tokio::test]
521 async fn test_smtp_server_new() {
522 let config = SmtpConfig::default();
523 let registry = Arc::new(SmtpSpecRegistry::new());
524 let server = SmtpServer::new(config, registry);
525 assert!(server.is_ok());
526 }
527
528 #[tokio::test]
529 async fn test_smtp_server_with_middleware() {
530 let config = SmtpConfig::default();
531 let registry = Arc::new(SmtpSpecRegistry::new());
532 let middleware = Arc::new(MiddlewareChain::new());
533 let server = SmtpServer::with_middleware(config, registry, middleware);
534 assert!(server.is_ok());
535 }
536}