aboutsummaryrefslogtreecommitdiff
path: root/src/smtp_server.rs
diff options
context:
space:
mode:
authorMroik <mroik@delayed.space>2026-04-09 23:54:57 +0200
committerMroik <mroik@delayed.space>2026-04-13 06:56:11 +0200
commit0ff548961a68213487faa85b7b01cf717bb27268 (patch)
tree01c711002904b29e3ca829c17d55d79d20a0fa6b /src/smtp_server.rs
parent3cedb01c7982c5a505a510f7af9a9601c39a5b97 (diff)
Rename modules to more accurate names
Signed-off-by: Mroik <mroik@delayed.space>
Diffstat (limited to 'src/smtp_server.rs')
-rw-r--r--src/smtp_server.rs242
1 files changed, 242 insertions, 0 deletions
diff --git a/src/smtp_server.rs b/src/smtp_server.rs
new file mode 100644
index 0000000..2795ecf
--- /dev/null
+++ b/src/smtp_server.rs
@@ -0,0 +1,242 @@
+use std::{
+ io::{BufRead, BufReader, Write},
+ net::{IpAddr, SocketAddr, TcpListener, TcpStream},
+};
+
+use anyhow::Result;
+use tokio::spawn;
+
+const SERVER_NAME: &str = "";
+
+pub struct SmtpServer {
+ listener: TcpListener,
+ running: bool,
+}
+
+impl SmtpServer {
+ pub fn new(ip: [u8; 4], port: u16) -> Result<Self> {
+ Ok(Self {
+ listener: TcpListener::bind((IpAddr::from(ip), port))?,
+ running: false,
+ })
+ }
+
+ // TODO: trap SIGINT to stop server?
+ pub async fn run(&mut self) -> Result<()> {
+ self.running = true;
+ while self.running {
+ let (stream, addr) = self.listener.accept()?;
+ let session = SessionHandler {
+ addr,
+ state: SessionState::default(),
+ client_host: String::new(),
+ from: None,
+ to: Vec::new(),
+ data: String::new(),
+ };
+ spawn(session.run(stream));
+ }
+ Ok(())
+ }
+}
+
+struct SessionHandler {
+ addr: SocketAddr,
+ state: SessionState,
+ client_host: String,
+ from: Option<String>,
+ to: Vec<String>,
+ data: String,
+}
+
+impl SessionHandler {
+ async fn run(mut self, stream: TcpStream) -> Result<()> {
+ let mut writer = stream.try_clone()?;
+ let mut r = BufReader::new(&stream);
+ let mut buffer = String::new();
+
+ writer.write_all(
+ Reply::Ready(String::from(SERVER_NAME))
+ .to_string()
+ .as_bytes(),
+ )?;
+
+ loop {
+ buffer.clear();
+ if r.read_line(&mut buffer)? == 0 {
+ break;
+ }
+ log::debug!("Received '{}' from '{}'", buffer.trim(), self.addr);
+
+ // In this state the server is not expecting commands as the input is part of the email
+ // message
+ if self.state == SessionState::AwaitingMailInput {
+ if buffer.starts_with(".\r\n") {
+ self.process_mail().await?;
+ continue;
+ }
+
+ self.data.push_str(&buffer);
+ continue;
+ }
+
+ let command = match Command::try_from(buffer.as_str()) {
+ Err(_) => {
+ writer.write_all(Reply::InvalidCommand.to_string().as_bytes())?;
+ continue;
+ }
+ Ok(v) => v,
+ };
+ let res = self.apply(command).await?;
+ writer.write_all(res.to_string().as_bytes())?;
+ }
+
+ log::info!("Connection closed by {}", self.addr);
+ Ok(())
+ }
+
+ async fn apply(&mut self, command: Command) -> Result<Reply> {
+ match &command {
+ Command::HELO(hostname) => self.helo(hostname).await,
+ Command::MAIL(from) => self.mail(from).await,
+ Command::RCPT(to) => self.rcpt(to).await,
+ Command::DATA => self.start_data().await,
+ }
+ }
+
+ /// HELO resets buffers
+ async fn helo(&mut self, hostname: &str) -> Result<Reply> {
+ self.client_host = String::from(hostname);
+ self.state = SessionState::Normal;
+ self.from = None;
+ self.to.clear();
+ Ok(Reply::Completed(String::from(SERVER_NAME)))
+ }
+
+ /// TODO: Validate email address
+ /// MAIL resets buffers
+ async fn mail(&mut self, from: &str) -> Result<Reply> {
+ if from.is_empty() {
+ return Ok(Reply::InvalidParameter);
+ }
+
+ self.from = Some(String::from(from));
+ self.to.clear();
+ self.state = SessionState::MailTransaction;
+
+ Ok(Reply::Completed(String::from("Ok")))
+ }
+
+ /// TODO: Validate email addresses
+ /// Only after having started a mail transaction
+ async fn rcpt(&mut self, to: &str) -> Result<Reply> {
+ if self.state != SessionState::MailTransaction {
+ return Ok(Reply::BadSequence);
+ }
+
+ if to.is_empty() {
+ return Ok(Reply::InvalidParameter);
+ }
+
+ self.to.push(String::from(to));
+ Ok(Reply::Completed(String::from("Ok")))
+ }
+
+ /// Only after having started a mail transaction
+ async fn start_data(&mut self) -> std::result::Result<Reply, anyhow::Error> {
+ if self.state != SessionState::MailTransaction {
+ return Ok(Reply::BadSequence);
+ }
+
+ // Should I return a more meaningful string with this error code?
+ if self.from.is_none() || self.to.is_empty() {
+ return Ok(Reply::InvalidCommand);
+ }
+
+ self.state = SessionState::AwaitingMailInput;
+
+ Ok(Reply::StartMailInput)
+ }
+
+ // TODO: Forward and store email
+ async fn process_mail(&mut self) -> Result<Reply> {
+ if self.from.is_none() || self.to.is_empty() || self.data.trim().is_empty() {
+ return Ok(Reply::InvalidCommand);
+ }
+
+ self.state = SessionState::Normal;
+ self.from = None;
+ self.to.clear();
+ self.data.clear();
+
+ todo!()
+ }
+}
+
+#[derive(PartialEq, Default)]
+enum SessionState {
+ #[default]
+ WaitingHelo,
+ MailTransaction,
+ AwaitingMailInput,
+ Normal,
+}
+
+enum Reply {
+ Ready(String),
+ Completed(String),
+ StartMailInput,
+ InvalidCommand,
+ InvalidParameter,
+ BadSequence,
+}
+
+impl std::fmt::Display for Reply {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Reply::Ready(hostname) => write!(f, "220 {}", hostname),
+ Reply::Completed(hostname) => write!(f, "250 {}", hostname),
+ Reply::InvalidCommand => write!(f, "500 Invalid command"),
+ Reply::InvalidParameter => write!(f, "501 Parameter error"),
+ Reply::BadSequence => write!(f, "503 Bad sequence of commands"),
+ Reply::StartMailInput => write!(f, "354 <CRLF>.<CRLF>"),
+ }
+ }
+}
+
+enum Command {
+ HELO(String),
+ MAIL(String),
+ RCPT(String),
+ DATA,
+}
+
+impl TryFrom<&str> for Command {
+ type Error = anyhow::Error;
+
+ fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
+ if value.len() >= 5 && value.to_lowercase().starts_with("helo ") {
+ return Ok(Command::HELO(String::from(&value[5..])));
+ }
+
+ if value.len() >= 11
+ && value.to_lowercase().starts_with("mail from:<")
+ && value.contains('>')
+ {
+ let from = &value[11..value.find('>').unwrap()];
+ return Ok(Command::MAIL(String::from(from)));
+ }
+
+ if value.len() >= 9 && value.to_lowercase().starts_with("rcpt to:<") && value.contains('>')
+ {
+ let to = &value[9..value.find('>').unwrap()];
+ return Ok(Command::RCPT(String::from(to)));
+ }
+
+ if value.to_lowercase().starts_with("data") {
+ return Ok(Command::DATA);
+ }
+
+ Err(anyhow::format_err!("Invalid command"))
+ }
+}
XMR address: 854DmXNrxULU3ZFJVs4Wc8PFhbq29RhqHhY8W6cdWrtFN3qmooKyyeYPcDzZTNRxphhJ5UzASQfAdEMwSteVqymk28aLhqj