aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/smtp_server.rs159
1 files changed, 158 insertions, 1 deletions
diff --git a/src/smtp_server.rs b/src/smtp_server.rs
index 83a1093..bee00e9 100644
--- a/src/smtp_server.rs
+++ b/src/smtp_server.rs
@@ -1,4 +1,7 @@
-use std::net::{IpAddr, SocketAddr};
+use std::{
+ fmt::Display,
+ net::{IpAddr, SocketAddr},
+};
use anyhow::Result;
use tokio::{
@@ -44,6 +47,7 @@ impl SmtpServer {
}
},
};
+ log::info!("Connected: {}", addr);
let session = SessionHandler {
addr,
server_name: self.server_name.clone(),
@@ -101,6 +105,9 @@ impl SessionHandler {
if self.state == SessionState::AwaitingMailInput {
if buffer.starts_with(".\r\n") {
self.process_mail().await?;
+ writer
+ .write_all(Reply::Completed("Ok").to_string().as_bytes())
+ .await?;
continue;
}
@@ -117,13 +124,19 @@ impl SessionHandler {
}
Ok(v) => v,
};
+ log::debug!("COMMAND: {}", command);
let res = self.apply(command).await?;
+ log::debug!("RESPONSE: {}", res);
writer.write_all(res.to_string().as_bytes()).await?;
if res == Reply::EndTransmission {
break;
}
}
+ // Should wait for the client before closing but meh. Any transaction has already gone
+ // through anyway so... waiting for the client to close the connection as part of the
+ // protocol was a dumb choice, the client can't issue new commands anyway.
+ stream.shutdown().await?;
Ok(())
}
@@ -292,3 +305,147 @@ impl<'a> TryFrom<&'a str> for Command<'a> {
Err(anyhow::format_err!("Invalid command"))
}
}
+
+impl Display for Command<'_> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Command::HELO(host) => write!(f, "HELO {}\r\n", host),
+ Command::MAIL(from) => write!(f, "MAIL FROM:<{}>\r\n", from),
+ Command::RCPT(to) => write!(f, "RCPT TO:<{}>\r\n", to),
+ Command::DATA => write!(f, "DATA\r\n"),
+ Command::QUIT => write!(f, "QUIT\r\n"),
+ }
+ }
+}
+
+mod tests {
+ use std::net::SocketAddr;
+
+ use tokio::{
+ io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
+ net::TcpSocket,
+ spawn,
+ sync::mpsc::channel,
+ };
+
+ use crate::smtp_server::{Command, SmtpServer};
+
+ const DOMAIN: &str = "mail.example.local";
+ const CLIENT_HOST: &str = "exploit";
+
+ #[tokio::test]
+ async fn receive_email() {
+ env_logger::init();
+ let mut server = SmtpServer::new([127, 0, 0, 1], 9090, DOMAIN).await.unwrap();
+ let (tx, mut rx) = channel(5);
+ let j = spawn(async move {
+ server.run(tx).await.unwrap();
+ log::info!("IM DONE");
+ });
+
+ let sock = TcpSocket::new_v4().unwrap();
+ let mut s = sock
+ .connect(SocketAddr::new([127, 0, 0, 1].into(), 9090))
+ .await
+ .unwrap();
+ let (reader, mut writer) = s.split();
+ let mut reader = BufReader::new(reader);
+ let mut buf = String::new();
+
+ assert!(reader.read_line(&mut buf).await.unwrap() > 0);
+ assert_eq!(buf.trim(), format!("220 {}", DOMAIN));
+
+ writer
+ .write_all(Command::HELO(CLIENT_HOST).to_string().as_bytes())
+ .await
+ .unwrap();
+ buf.clear();
+ assert!(reader.read_line(&mut buf).await.unwrap() > 0);
+ assert_eq!(buf.trim(), format!("250 {}", DOMAIN));
+
+ writer
+ .write_all(Command::MAIL("mroik@poul.org").to_string().as_bytes())
+ .await
+ .unwrap();
+ buf.clear();
+ assert!(reader.read_line(&mut buf).await.unwrap() > 0);
+ assert_eq!(buf.trim(), "250 Ok");
+
+ writer
+ .write_all(Command::RCPT("mroik@delayed.space").to_string().as_bytes())
+ .await
+ .unwrap();
+ buf.clear();
+ assert!(reader.read_line(&mut buf).await.unwrap() > 0);
+ assert_eq!(buf.trim(), "250 Ok");
+
+ writer
+ .write_all(Command::DATA.to_string().as_bytes())
+ .await
+ .unwrap();
+ buf.clear();
+ assert!(reader.read_line(&mut buf).await.unwrap() > 0);
+ assert_eq!(buf.trim(), "354 <CRLF>.<CRLF>");
+
+ writer.write_all(b"From: mroik@poul.org\r\n").await.unwrap();
+ writer
+ .write_all(b"To: mroik@delayed.space\r\n")
+ .await
+ .unwrap();
+ writer
+ .write_all(b"Date: Tue, 14 Apr 2026 23:00:00 +0200\r\n")
+ .await
+ .unwrap();
+ writer
+ .write_all(b"Subject: This is a test\r\n")
+ .await
+ .unwrap();
+ writer.write_all(b"\r\n").await.unwrap();
+ writer
+ .write_all(b"Hey, this is a test. You can ignore this!\r\n")
+ .await
+ .unwrap();
+ writer.write_all(b"\r\n").await.unwrap();
+ writer.write_all(b"Mroik\r\n").await.unwrap();
+ writer.write_all(b".\r\n").await.unwrap();
+ buf.clear();
+ assert!(reader.read_line(&mut buf).await.unwrap() > 0);
+ assert_eq!(buf.trim(), "250 Ok");
+
+ let lines = [
+ "From: mroik@poul.org",
+ "To: mroik@delayed.space",
+ "Date: Tue, 14 Apr 2026 23:00:00 +0200",
+ "Subject: This is a test",
+ "",
+ "Hey, this is a test. You can ignore this!",
+ "",
+ "Mroik",
+ ];
+
+ let email = rx.recv().await.unwrap();
+ assert_eq!(email.from, "mroik@poul.org");
+ assert_eq!(email.recipient.len(), 1);
+ assert_eq!(email.recipient[0], "mroik@delayed.space");
+ assert_eq!(email.data.trim(), lines.join("\r\n").trim());
+
+ // Close connection
+ writer
+ .write_all(Command::QUIT.to_string().as_bytes())
+ .await
+ .unwrap();
+ buf.clear();
+ assert!(reader.read_line(&mut buf).await.unwrap() > 0);
+ assert_eq!(buf.trim(), "221 OK");
+
+ buf.clear();
+ assert_eq!(reader.read_line(&mut buf).await.unwrap(), 0);
+
+ s.shutdown().await.unwrap();
+ // This is necessary because the server continues to run even after closing a connection as
+ // it continues to listen for new connections.
+ //
+ // In order to ensure that this doesn't affect other tests we have to abort it.
+ j.abort();
+ }
+}
XMR address: 854DmXNrxULU3ZFJVs4Wc8PFhbq29RhqHhY8W6cdWrtFN3qmooKyyeYPcDzZTNRxphhJ5UzASQfAdEMwSteVqymk28aLhqj