Fix UTF-8 decoding issue in STARTTLS implementation

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

copilot-swe-agent[bot] committed Oct 9, 2025 at 13:17 UTC 50616ffe1fc132686de1a0386ecae99b411cff7b
4 files changed +23 -24
src/generate_certificate.rs
+5 -2
@@ -8,7 +8,7 @@ use openssl::{
8 },
9 };
10
11 -pub fn generate_certificate() -> anyhow::Result<(Vec<u8>, Vec<u8>)> {
11 +pub fn generate_certificate() -> anyhow::Result<(String, String)> {
12 let cert_result: Result<(Vec<u8>, Vec<u8>), openssl::error::ErrorStack> = {
13 let rsa = Rsa::generate(4096)?;
14 let pkey = PKey::from_rsa(rsa)?;
@@ -40,5 +40,8 @@ pub fn generate_certificate() -> anyhow::Result<(Vec<u8>, Vec<u8>)> {
40 };
41 let (pem_certificate, pem_private_key) = cert_result?;
42
43 - Ok((pem_certificate, pem_private_key))
43 + Ok((
44 + String::from_utf8_lossy(&pem_certificate).to_string(),
45 + String::from_utf8_lossy(&pem_private_key).to_string(),
46 + ))
47 }
src/handle_session.rs
+7 -4
@@ -1,6 +1,6 @@
1 -use futures::{SinkExt, StreamExt, stream::iter};
2 -use tokio::net::TcpStream;
3 -use tokio_util::codec::{Framed, LinesCodec, LinesCodecError};
1 +use futures::{StreamExt};
2 +use tokio::io::{AsyncRead, AsyncWrite};
3 +use tokio_util::codec::{Framed, LinesCodec};
4
5 use crate::send_commands::send_commands;
6
@@ -10,7 +10,10 @@ enum SmtpState {
10 Quit,
11 }
12
13 -pub async fn handle_session(mut stream: TcpStream) -> anyhow::Result<()> {
13 +pub async fn handle_session<S>(stream: S) -> anyhow::Result<()>
14 +where
15 + S: AsyncRead + AsyncWrite + Unpin,
16 +{
17 let RE_SMTP_MAIL = regex::Regex::new(r"(?i)from: ?<(.+)>").unwrap();
18 let RE_SMTP_RCPT = regex::Regex::new(r"(?i)to: ?<(.+)>").unwrap();
19 let mut message = String::new();
src/handle_starttls.rs
+4 -13
@@ -2,28 +2,19 @@ use native_tls::{Identity, TlsAcceptor};
2 use tokio::net::TcpStream;
3 use tokio_native_tls::TlsAcceptor as TokioTlsAcceptor;
4
5 -use crate::generate_certificate::generate_certificate;
5 +use crate::{generate_certificate::generate_certificate, handle_session::handle_session};
6
7 pub async fn handle_starttls(stream: TcpStream) -> anyhow::Result<()> {
8 // ideally the certificate should only be loaded from here and not generated each time
9 let (pem_certificate, pem_private_key) = generate_certificate()?;
10 - let identity = Identity::from_pkcs8(&pem_certificate, &pem_private_key)?;
10 + let identity = Identity::from_pkcs8(pem_certificate.as_bytes(), pem_private_key.as_bytes())?;
11 let tls_acceptor = TlsAcceptor::builder(identity).build()?;
12 let tls_acceptor = TokioTlsAcceptor::from(tls_acceptor);
13
14 match tls_acceptor.accept(stream).await {
15 Ok(tls_stream) => {
16 - // we can now handle the normal SMTP session over TLS
17 - // For now, we'll just create a framed connection to handle encrypted SMTP
18 - use tokio_util::codec::{Framed, LinesCodec};
19 - use futures::StreamExt;
20 -
21 - let mut framed = Framed::new(tls_stream, LinesCodec::new());
22 - while let Some(line_str) = framed.next().await {
23 - let _line = line_str?;
24 - // Basic echo for now - in a real implementation, this would handle SMTP commands
25 - break;
26 - }
16 + // we can now handle the normal SMTP session
17 + handle_session(tls_stream).await?;
18 }
19 Err(e) => {
20 tracing::error!("Error establishing SMTP TLS connection: {:?}", e);
src/send_commands.rs
+7 -5
@@ -1,12 +1,14 @@
1 use futures::{SinkExt, StreamExt, stream::iter};
2 -use tokio::net::TcpStream;
3 -use tokio_native_tls::TlsStream;
2 +use tokio::io::{AsyncRead, AsyncWrite};
3 use tokio_util::codec::{Framed, LinesCodec};
4
6 -pub async fn send_commands(
7 - framed: &mut Framed<TcpStream, LinesCodec>,
5 +pub async fn send_commands<S>(
6 + framed: &mut Framed<S, LinesCodec>,
7 commands: Vec<String>,
9 -) -> anyhow::Result<()> {
8 +) -> anyhow::Result<()>
9 +where
10 + S: AsyncRead + AsyncWrite + Unpin,
11 +{
12 // only need to add \r because the codec only adds \n
13 let messages = iter(commands.into_iter().map(|x| format!("{}\r", x)));
14 framed.send_all(&mut messages.map(Ok)).await?;