Implement separation of incoming and outgoing mail handling

Co-authored-by: setoelkahfi <1797197+setoelkahfi@users.noreply.github.com>

copilot-swe-agent[bot] committed Oct 14, 2025 at 11:02 UTC 6c8e4e0826215cf734c38abcee64854dc4296599
7 files changed +98 -28
src/handle_session.rs
+29 -12
@@ -1,5 +1,6 @@
1 use crate::send_commands::send_commands;
2 use crate::storage::MessageStorage;
3 +use crate::mail_mode::MailMode;
4 use futures::StreamExt;
5 use tokio::io::{AsyncRead, AsyncWrite};
6 use tokio_util::codec::{Framed, LinesCodec};
@@ -10,7 +11,7 @@ enum SmtpState {
11 Quit,
12 }
13
13 -pub async fn handle_session<S>(stream: S, storage: Option<MessageStorage>) -> anyhow::Result<()>
14 +pub async fn handle_session<S>(stream: S, storage: Option<MessageStorage>, mode: MailMode) -> anyhow::Result<()>
15 where
16 S: AsyncRead + AsyncWrite + Unpin,
17 {
@@ -102,18 +103,34 @@ where
103 // The end of the email content has been received
104 send_commands(&mut framed, vec!["250 OK".to_string()]).await?;
105
105 - // Log the received email
106 - tracing::info!(
107 - "Email received - From: {:?}, To: {:?}, Size: {} bytes",
108 - mailfrom,
109 - rcpts,
110 - message.len()
111 - );
106 + // Handle message based on mode
107 + match mode {
108 + MailMode::Receive => {
109 + // Log the received email
110 + tracing::info!(
111 + "Email received - From: {:?}, To: {:?}, Size: {} bytes",
112 + mailfrom,
113 + rcpts,
114 + message.len()
115 + );
116
113 - // Store the message if storage is available
114 - if let Some(ref storage) = storage {
115 - if let Some(ref from) = mailfrom {
116 - storage.store_message(from.clone(), rcpts.clone(), message.clone());
117 + // Store the message in INBOX if storage is available
118 + if let Some(ref storage) = storage {
119 + if let Some(ref from) = mailfrom {
120 + storage.store_message(from.clone(), rcpts.clone(), message.clone());
121 + }
122 + }
123 + }
124 + MailMode::Submit => {
125 + // Log the outbound mail relay
126 + tracing::info!(
127 + "Outbound mail relay - From: {:?}, To: {:?}, Size: {} bytes",
128 + mailfrom,
129 + rcpts,
130 + message.len()
131 + );
132 + // In a real implementation, this would relay the message to external servers
133 + // For now, we just log the submission
134 }
135 }
136
src/handle_starttls.rs
+3 -3
@@ -1,9 +1,9 @@
1 -use crate::{generate_certificate::generate_certificate, handle_session::handle_session, storage::MessageStorage};
1 +use crate::{generate_certificate::generate_certificate, handle_session::handle_session, storage::MessageStorage, mail_mode::MailMode};
2 use native_tls::{Identity, TlsAcceptor};
3 use tokio::net::TcpStream;
4 use tokio_native_tls::TlsAcceptor as TokioTlsAcceptor;
5
6 -pub async fn handle_starttls(stream: &mut TcpStream, storage: Option<MessageStorage>) -> anyhow::Result<()> {
6 +pub async fn handle_starttls(stream: &mut TcpStream, storage: Option<MessageStorage>, mode: MailMode) -> anyhow::Result<()> {
7 // ideally the certificate should only be loaded from here and not generated each time
8 let (pem_certificate, pem_private_key) = generate_certificate()?;
9 let identity = Identity::from_pkcs8(pem_certificate.as_bytes(), pem_private_key.as_bytes())?;
@@ -13,7 +13,7 @@ pub async fn handle_starttls(stream: &mut TcpStream, storage: Option<MessageStor
13 match tls_acceptor.accept(stream).await {
14 Ok(tls_stream) => {
15 // we can now handle the normal SMTP session
16 - handle_session(tls_stream, storage).await?;
16 + handle_session(tls_stream, storage, mode).await?;
17 }
18 Err(e) => {
19 tracing::error!("Error establishing SMTP TLS connection: {:?}", e);
src/handle_unsecured_session.rs
+3 -3
@@ -1,10 +1,10 @@
1 -use crate::{handle_starttls::handle_starttls, storage::MessageStorage};
1 +use crate::{handle_starttls::handle_starttls, storage::MessageStorage, mail_mode::MailMode};
2 use tokio::{
3 io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter},
4 net::TcpStream,
5 };
6
7 -pub async fn handle_unsecured_session(stream: &mut TcpStream, storage: Option<MessageStorage>) -> anyhow::Result<()> {
7 +pub async fn handle_unsecured_session(stream: &mut TcpStream, storage: Option<MessageStorage>, mode: MailMode) -> anyhow::Result<()> {
8 let (reader, writer) = stream.split();
9 let mut reader = BufReader::new(reader);
10 let mut writer = BufWriter::new(writer);
@@ -52,7 +52,7 @@ pub async fn handle_unsecured_session(stream: &mut TcpStream, storage: Option<Me
52 }
53
54 if is_tls {
55 - handle_starttls(stream, storage).await?;
55 + handle_starttls(stream, storage, mode).await?;
56 }
57
58 Ok(())
src/lib.rs
+1
@@ -6,3 +6,4 @@ pub mod listen;
6 pub mod send_commands;
7 pub mod imap;
8 pub mod storage;
9 +pub mod mail_mode;
src/listen.rs
+11 -7
@@ -1,20 +1,24 @@
1 -use crate::{handle_unsecured_session::handle_unsecured_session, storage::MessageStorage};
1 +use crate::{handle_unsecured_session::handle_unsecured_session, storage::MessageStorage, mail_mode::MailMode};
2 use std::net::SocketAddr;
3 use tokio::io::AsyncWriteExt;
4 use tokio::net::{TcpListener, TcpStream};
5
6 -pub async fn listen(storage: MessageStorage) -> anyhow::Result<()> {
7 - let addr = SocketAddr::from(([127, 0, 0, 1], 2525));
6 +pub async fn listen(storage: MessageStorage, port: u16, mode: MailMode) -> anyhow::Result<()> {
7 + let addr = SocketAddr::from(([127, 0, 0, 1], port));
8 let listener = TcpListener::bind(addr).await?;
9
10 - tracing::info!("SMTP server listening on {}", addr);
10 + let mode_str = match mode {
11 + MailMode::Receive => "RECEIVE",
12 + MailMode::Submit => "SUBMIT",
13 + };
14 + tracing::info!("SMTP server listening on {} (mode: {})", addr, mode_str);
15
16 loop {
17 match listener.accept().await {
18 Ok((mut stream, _)) => {
19 let storage_clone = storage.clone();
20 tokio::spawn(async move {
17 - if let Err(err) = handle_connection(&mut stream, storage_clone).await {
21 + if let Err(err) = handle_connection(&mut stream, storage_clone, mode).await {
22 tracing::error!("Error handling SMTP connection: {:?}", err);
23 };
24 });
@@ -26,10 +30,10 @@ pub async fn listen(storage: MessageStorage) -> anyhow::Result<()> {
30 }
31 }
32
29 -async fn handle_connection(stream: &mut TcpStream, storage: MessageStorage) -> anyhow::Result<()> {
33 +async fn handle_connection(stream: &mut TcpStream, storage: MessageStorage, mode: MailMode) -> anyhow::Result<()> {
34 // Send greeting
35 stream.write_all(b"220 My SMTP server\r\n").await?;
36
37 // Handle unsecured session which will handle STARTTLS
34 - handle_unsecured_session(stream, Some(storage)).await
38 + handle_unsecured_session(stream, Some(storage), mode).await
39 }
src/mail_mode.rs new
+30
@@ -0,0 +1,30 @@
1 +/// Enum to distinguish between different SMTP mail handling modes
2 +#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3 +pub enum MailMode {
4 + /// Receive mode: for incoming mail (port 2525)
5 + /// Messages received in this mode are stored in INBOX
6 + Receive,
7 +
8 + /// Submit mode: for outgoing mail submission (port 2526)
9 + /// Messages received in this mode are logged and relayed
10 + Submit,
11 +}
12 +
13 +#[cfg(test)]
14 +mod tests {
15 + use super::*;
16 +
17 + #[test]
18 + fn test_mail_mode_equality() {
19 + assert_eq!(MailMode::Receive, MailMode::Receive);
20 + assert_eq!(MailMode::Submit, MailMode::Submit);
21 + assert_ne!(MailMode::Receive, MailMode::Submit);
22 + }
23 +
24 + #[test]
25 + fn test_mail_mode_clone() {
26 + let mode1 = MailMode::Receive;
27 + let mode2 = mode1;
28 + assert_eq!(mode1, mode2);
29 + }
30 +}
src/main.rs
+21 -3
@@ -1,5 +1,5 @@
1 use anyhow::Result;
2 -use smbcloud_mail::{listen::listen, storage::MessageStorage};
2 +use smbcloud_mail::{listen::listen, storage::MessageStorage, mail_mode::MailMode};
3
4 #[tokio::main]
5 async fn main() -> Result<()> {
@@ -19,6 +19,24 @@ async fn main() -> Result<()> {
19 }
20 });
21
22 - // Start SMTP server (this will block)
23 - listen(storage).await
22 + // Clone storage for submission SMTP server (port 2526)
23 + let submit_storage = storage.clone();
24 +
25 + // Start submission SMTP server (port 2526) in a separate task
26 + // Port 2526: Mail submission port for outgoing mail
27 + // - Used by mail clients to submit outgoing messages
28 + // - Messages are logged and would be relayed to external mail servers
29 + // - In Submit mode, messages are not stored in INBOX
30 + tokio::spawn(async move {
31 + if let Err(err) = listen(submit_storage, 2526, MailMode::Submit).await {
32 + tracing::error!("SMTP submission server error: {:?}", err);
33 + }
34 + });
35 +
36 + // Start receiving SMTP server (port 2525) - this will block
37 + // Port 2525: Mail receiving port for incoming mail
38 + // - Used to receive incoming messages from other mail servers
39 + // - Messages are stored in the INBOX mailbox
40 + // - In Receive mode, messages are persisted for IMAP access
41 + listen(storage, 2525, MailMode::Receive).await
42 }