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
}