format
Seto committed
Oct 9, 2025 at 22:38 UTC
c67c1de74f239e459666bf949a627c836e4d7c7e
8 files changed
+33
-41
Cargo.toml
+6
-6
@@ -4,13 +4,13 @@ version = "0.1.0"
4
edition = "2021"
5
6
[dependencies]
7
+anyhow = "1"
8
+futures = "0.3"
9
+native-tls = "0.2"
10
+openssl = "0.10"
11
+regex = "1"
12
tokio = { version = "1", features = [ "macros","io-util","net","rt", "rt-multi-thread"] }
8
-tokio-util = { version = "0.7", features = ["codec"] }
13
tokio-native-tls = "0.3"
10
-native-tls = "0.2"
11
-anyhow = "1"
14
+tokio-util = { version = "0.7", features = ["codec"] }
15
tracing = "0.1"
16
tracing-subscriber = "0.3"
14
-regex = "1"
15
-futures = "0.3"
16
-openssl = "0.10"
src/generate_certificate.rs
+1
-1
@@ -3,8 +3,8 @@ use openssl::{
3
pkey::PKey,
4
rsa::Rsa,
5
x509::{
6
- X509, X509NameBuilder,
6
extension::{AuthorityKeyIdentifier, BasicConstraints, SubjectKeyIdentifier},
7
+ X509NameBuilder, X509,
8
},
9
};
10
src/handle_session.rs
+18
-21
@@ -1,9 +1,8 @@
1
-use futures::{StreamExt};
1
+use crate::send_commands::send_commands;
2
+use futures::StreamExt;
3
use tokio::io::{AsyncRead, AsyncWrite};
4
use tokio_util::codec::{Framed, LinesCodec};
5
5
-use crate::send_commands::send_commands;
6
-
6
enum SmtpState {
7
Command,
8
Data,
@@ -14,8 +13,8 @@ pub async fn handle_session<S>(stream: S) -> anyhow::Result<()>
13
where
14
S: AsyncRead + AsyncWrite + Unpin,
15
{
17
- let RE_SMTP_MAIL = regex::Regex::new(r"(?i)from: ?<(.+)>").unwrap();
18
- let RE_SMTP_RCPT = regex::Regex::new(r"(?i)to: ?<(.+)>").unwrap();
16
+ let re_smtp_mail = regex::Regex::new(r"(?i)from: ?<(.+)>").unwrap();
17
+ let re_smtp_rcpt = regex::Regex::new(r"(?i)to: ?<(.+)>").unwrap();
18
let mut message = String::new();
19
let mut state = SmtpState::Command;
20
let mut mailfrom: Option<String> = None;
@@ -34,7 +33,7 @@ where
33
}
34
"MAIL" => {
35
// Handle MAIL FROM command
37
- if let Some(address) = RE_SMTP_MAIL.captures(arg).and_then(|cap| cap.get(1))
36
+ if let Some(address) = re_smtp_mail.captures(arg).and_then(|cap| cap.get(1))
37
{
38
mailfrom = Some(address.as_str().to_string());
39
send_commands(&mut framed, vec!["250 OK".to_string()]).await?;
@@ -54,19 +53,17 @@ where
53
vec!["503 Error: Send MAIL first".to_string()],
54
)
55
.await?;
56
+ } else if let Some(address) =
57
+ re_smtp_rcpt.captures(arg).and_then(|cap| cap.get(1))
58
+ {
59
+ rcpts.push(address.as_str().to_string());
60
+ send_commands(&mut framed, vec!["250 OK".to_string()]).await?;
61
} else {
58
- if let Some(address) =
59
- RE_SMTP_RCPT.captures(arg).and_then(|cap| cap.get(1))
60
- {
61
- rcpts.push(address.as_str().to_string());
62
- send_commands(&mut framed, vec!["250 OK".to_string()]).await?;
63
- } else {
64
- send_commands(
65
- &mut framed,
66
- vec!["501 Syntax: RCPT TO: <address>".to_string()],
67
- )
68
- .await?;
69
- }
62
+ send_commands(
63
+ &mut framed,
64
+ vec!["501 Syntax: RCPT TO: <address>".to_string()],
65
+ )
66
+ .await?;
67
}
68
}
69
"DATA" => {
@@ -103,7 +100,7 @@ where
100
if line.trim() == "." {
101
// The end of the email content has been received
102
send_commands(&mut framed, vec!["250 OK".to_string()]).await?;
106
-
103
+
104
// Log the received email
105
tracing::info!(
106
"Email received - From: {:?}, To: {:?}, Size: {} bytes",
@@ -111,7 +108,7 @@ where
108
rcpts,
109
message.len()
110
);
114
-
111
+
112
// reset the state and variables for the next email
113
mailfrom = None;
114
rcpts = Vec::new();
@@ -120,7 +117,7 @@ where
117
} else {
118
// Add the received line to the email content
119
message.push_str(&line);
123
- message.push_str("\n");
120
+ message.push('\n');
121
}
122
}
123
SmtpState::Quit => {
src/handle_starttls.rs
+1
-2
@@ -1,9 +1,8 @@
1
+use crate::{generate_certificate::generate_certificate, handle_session::handle_session};
2
use native_tls::{Identity, TlsAcceptor};
3
use tokio::net::TcpStream;
4
use tokio_native_tls::TlsAcceptor as TokioTlsAcceptor;
5
5
-use crate::{generate_certificate::generate_certificate, handle_session::handle_session};
6
-
6
pub async fn handle_starttls(stream: &mut TcpStream) -> 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()?;
src/handle_unsecured_session.rs
+3
-6
@@ -1,13 +1,10 @@
1
+use crate::handle_starttls::handle_starttls;
2
use tokio::{
3
io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter},
4
net::TcpStream,
5
};
6
6
-use crate::handle_starttls::handle_starttls;
7
-
8
-pub async fn handle_unsecured_session(
9
- stream: &mut TcpStream,
10
-) -> anyhow::Result<()> {
7
+pub async fn handle_unsecured_session(stream: &mut TcpStream) -> anyhow::Result<()> {
8
let (reader, writer) = stream.split();
9
let mut reader = BufReader::new(reader);
10
let mut writer = BufWriter::new(writer);
@@ -57,6 +54,6 @@ pub async fn handle_unsecured_session(
54
if is_tls {
55
handle_starttls(stream).await?;
56
}
60
-
57
+
58
Ok(())
59
}
src/listen.rs
+2
-3
@@ -1,9 +1,8 @@
1
+use crate::handle_unsecured_session::handle_unsecured_session;
2
use std::net::SocketAddr;
3
use tokio::io::AsyncWriteExt;
4
use tokio::net::{TcpListener, TcpStream};
5
5
-use crate::handle_unsecured_session::handle_unsecured_session;
6
-
6
pub async fn listen() -> anyhow::Result<()> {
7
let addr = SocketAddr::from(([127, 0, 0, 1], 2525));
8
let listener = TcpListener::bind(addr).await?;
@@ -29,7 +28,7 @@ pub async fn listen() -> anyhow::Result<()> {
28
async fn handle_connection(stream: &mut TcpStream) -> anyhow::Result<()> {
29
// Send greeting
30
stream.write_all(b"220 My SMTP server\r\n").await?;
32
-
31
+
32
// Handle unsecured session which will handle STARTTLS
33
handle_unsecured_session(stream).await
34
}
src/main.rs
+1
-1
@@ -5,6 +5,6 @@ use smbcloud_mail::listen::listen;
5
async fn main() -> Result<()> {
6
// Initialize tracing subscriber for logging
7
tracing_subscriber::fmt::init();
8
-
8
+
9
listen().await
10
}
src/send_commands.rs
+1
-1
@@ -1,4 +1,4 @@
1
-use futures::{SinkExt, StreamExt, stream::iter};
1
+use futures::{stream::iter, SinkExt, StreamExt};
2
use tokio::io::{AsyncRead, AsyncWrite};
3
use tokio_util::codec::{Framed, LinesCodec};
4