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 { 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(), }; spawn(session.run(stream)); } Ok(()) } } struct SessionHandler { addr: SocketAddr, state: SessionState, client_host: 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 { if r.read_line(&mut buffer)? == 0 { break; } log::debug!("Received '{}' from '{}'", buffer.trim(), self.addr); let command = Command::try_from(buffer.as_str())?; let res = self.apply(command).await?; writer.write_all(res.to_string().as_bytes())?; buffer.clear(); } log::info!("Connection closed by {}", self.addr); Ok(()) } async fn apply(&mut self, command: Command) -> Result { match &command { Command::HELO(hostname) => self.helo(hostname).await, } } /// HELO resets buffers async fn helo(&mut self, hostname: &str) -> Result { self.client_host = String::from(hostname); self.state = SessionState::NextState; Ok(Reply::Completed(String::from(SERVER_NAME))) } } enum SessionState { WaitingHelo, /// This is a dev state NextState, } impl Default for SessionState { fn default() -> Self { Self::WaitingHelo } } enum Reply { Ready(String), Completed(String), } impl ToString for Reply { fn to_string(&self) -> String { match self { Reply::Ready(hostname) => format!("220 {}", hostname), Reply::Completed(hostname) => format!("250 {}", hostname), } } } enum Command { HELO(String), } impl TryFrom<&str> for Command { type Error = anyhow::Error; fn try_from(value: &str) -> std::result::Result { if value.len() >= 5 && value.to_lowercase().starts_with("helo ") { return Ok(Command::HELO(String::from(&value[5..]))); } Err(anyhow::format_err!("Invalid command")) } }