From a710a88db5eceafb9f006be09e60ed16e4768b60 Mon Sep 17 00:00:00 2001 From: Wez Furlong Date: Thu, 16 Feb 2023 18:09:55 -0700 Subject: [PATCH] cut over smtp server to new command parser --- Cargo.lock | 1 + crates/kumod/Cargo.toml | 1 + crates/kumod/src/smtp_server.rs | 122 +++++--------------------------- crates/rfc5321/src/parser.rs | 47 ++++++++++++ 4 files changed, 67 insertions(+), 104 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5cc1bfdf..9ad46e0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -849,6 +849,7 @@ dependencies = [ "mail-send", "message", "mlua", + "rfc5321", "ringbuf", "rustls", "serde", diff --git a/crates/kumod/Cargo.toml b/crates/kumod/Cargo.toml index c8344f0a..76bd8eea 100644 --- a/crates/kumod/Cargo.toml +++ b/crates/kumod/Cargo.toml @@ -15,6 +15,7 @@ mail-auth = "0.3" mail-send = "0.3" message = {path="../message"} mlua = {version="0.8", features=["vendored", "lua54", "async", "send", "serialize"]} +rfc5321 = {path="../rfc5321"} ringbuf = "0.3" rustls = "0.20" serde = {version="1.0", features=["derive"]} diff --git a/crates/kumod/src/smtp_server.rs b/crates/kumod/src/smtp_server.rs index ec43870f..0b8b9975 100644 --- a/crates/kumod/src/smtp_server.rs +++ b/crates/kumod/src/smtp_server.rs @@ -4,6 +4,7 @@ use crate::spool::SpoolManager; use anyhow::anyhow; use message::{EnvelopeAddress, Message}; use mlua::ToLuaMulti; +use rfc5321::Command; use std::fmt::Debug; use std::sync::Arc; use thiserror::Error; @@ -165,6 +166,8 @@ impl SmtpServer { // TODO: we are supposed to report extension commands in our EHLO // response, but we don't have any yet. + let domain = domain.to_string(); + if let Err(rej) = self .call_callback("smtp_server_ehlo", domain.clone()) .await? @@ -178,6 +181,8 @@ impl SmtpServer { self.said_hello.replace(domain); } Ok(Command::Helo(domain)) => { + let domain = domain.to_string(); + if let Err(rej) = self .call_callback("smtp_server_ehlo", domain.clone()) .await? @@ -188,12 +193,16 @@ impl SmtpServer { self.write_response(250, format!("Hello {domain}!")).await?; self.said_hello.replace(domain); } - Ok(Command::Mail(address)) => { + Ok(Command::MailFrom { + address, + parameters: _, + }) => { if self.state.is_some() { self.write_response(503, "MAIL FROM already issued; you must RSET first") .await?; continue; } + let address = EnvelopeAddress::parse(&address.to_string())?; if let Err(rej) = self .call_callback("smtp_server_mail_from", address.clone()) .await? @@ -208,12 +217,16 @@ impl SmtpServer { }); self.write_response(250, format!("OK {address:?}")).await?; } - Ok(Command::Rcpt(address)) => { + Ok(Command::RcptTo { + address, + parameters: _, + }) => { if self.state.is_none() { self.write_response(503, "MAIL FROM must be issued first") .await?; continue; } + let address = EnvelopeAddress::parse(&address.to_string())?; if let Err(rej) = self .call_callback("smtp_server_mail_rcpt_to", address.clone()) .await? @@ -327,113 +340,14 @@ impl SmtpServer { self.state.take(); self.write_response(250, "Reset state").await?; } - Ok(Command::Noop) => { + Ok(Command::Noop(_)) => { self.write_response(250, "the goggles do nothing").await?; } - Ok(Command::Unknown(cmd)) => { - self.write_response(502, format!("Command unrecognized/unimplemented: {cmd}")) + Ok(Command::Vrfy(_) | Command::Expn(_) | Command::Help(_)) => { + self.write_response(502, format!("Command unimplemented")) .await?; } } } } } - -#[derive(Debug, Clone, PartialEq)] -enum Command { - Ehlo(String), - Helo(String), - Mail(EnvelopeAddress), - Rcpt(EnvelopeAddress), - Data, - Rset, - Noop, - Quit, - Unknown(String), -} - -impl Command { - fn parse(line: &str) -> anyhow::Result { - fn prefix_match(line: &str, candidate: &str) -> bool { - if line.len() < candidate.len() { - false - } else { - line[..candidate.len()].eq_ignore_ascii_case(candidate) - } - } - - fn extract_envelope(line: &str) -> anyhow::Result<(&str, &str)> { - if !line.starts_with('<') { - anyhow::bail!("expected <: {line:?}"); - } - let rangle = line - .bytes() - .position(|c| c == b'>') - .ok_or_else(|| anyhow::anyhow!("expected >: {line:?}"))?; - - Ok((&line[1..rangle], &line[rangle + 1..])) - } - - Ok(if line.eq_ignore_ascii_case("QUIT") { - Self::Quit - } else if line.eq_ignore_ascii_case("DATA") { - Self::Data - } else if line.eq_ignore_ascii_case("RSET") { - Self::Rset - } else if line.eq_ignore_ascii_case("NOOP") { - Self::Noop - } else if prefix_match(line, "EHLO ") { - Self::Ehlo(line[5..].to_string()) - } else if prefix_match(line, "HELO ") { - Self::Helo(line[5..].to_string()) - } else if prefix_match(line, "MAIL FROM:") { - let (address, _params) = extract_envelope(&line[10..])?; - // TODO: MAIL FROM can accept key/value parameters - Self::Mail(EnvelopeAddress::parse(address)?) - } else if prefix_match(line, "RCPT TO:") { - let (address, _params) = extract_envelope(&line[8..])?; - if address.is_empty() { - anyhow::bail!("Null sender not permitted as a recipient"); - } - Self::Rcpt(EnvelopeAddress::parse(address)?) - } else { - Self::Unknown(line.to_string()) - }) - } -} - -#[cfg(test)] -mod test { - use super::*; - use k9::assert_equal; - - #[test] - fn command_parser() { - assert_equal!(Command::parse("QUIT").unwrap(), Command::Quit); - assert_equal!(Command::parse("quit").unwrap(), Command::Quit); - assert_equal!( - Command::parse("quite").unwrap(), - Command::Unknown("quite".to_string()) - ); - assert_equal!( - Command::parse("flibble").unwrap(), - Command::Unknown("flibble".to_string()) - ); - assert_equal!( - Command::parse("MAIL From:<>").unwrap(), - Command::Mail(EnvelopeAddress::null_sender()) - ); - assert_equal!( - Command::parse("MAIL From:").unwrap(), - Command::Mail(EnvelopeAddress::parse("user@example.com").unwrap()) - ); - assert_equal!( - Command::parse("rcpt to:<>").unwrap_err().to_string(), - "Null sender not permitted as a recipient" - ); - assert_equal!( - Command::parse("rcpt TO:").unwrap(), - Command::Rcpt(EnvelopeAddress::parse("user@example.com").unwrap()) - ); - } -} diff --git a/crates/rfc5321/src/parser.rs b/crates/rfc5321/src/parser.rs index 1aab8e38..6d1e53d2 100644 --- a/crates/rfc5321/src/parser.rs +++ b/crates/rfc5321/src/parser.rs @@ -170,24 +170,60 @@ pub enum ReversePath { NullSender, } +impl ToString for ReversePath { + fn to_string(&self) -> String { + match self { + Self::Path(p) => p.to_string(), + Self::NullSender => "".to_string(), + } + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum ForwardPath { Path(MailPath), Postmaster, } +impl ToString for ForwardPath { + fn to_string(&self) -> String { + match self { + Self::Path(p) => p.to_string(), + Self::Postmaster => "postmaster".to_string(), + } + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct MailPath { pub at_domain_list: Vec, pub mailbox: Mailbox, } +impl ToString for MailPath { + fn to_string(&self) -> String { + // Note: RFC5321 says about at_domain_list: + // Note that this form, the so-called "source + // route", MUST BE accepted, SHOULD NOT be + // generated, and SHOULD be ignored. + // So we don't include it in the stringified + // version of MailPath + self.mailbox.to_string() + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct Mailbox { pub local_part: String, pub domain: Domain, } +impl ToString for Mailbox { + fn to_string(&self) -> String { + format!("{}@{}", self.local_part, self.domain.to_string()) + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum Domain { Name(String), @@ -196,6 +232,17 @@ pub enum Domain { Tagged { tag: String, literal: String }, } +impl ToString for Domain { + fn to_string(&self) -> String { + match self { + Self::Name(name) => name.to_string(), + Self::V4(addr) => format!("[{addr}]"), + Self::V6(addr) => format!("[IPv6:{addr}]"), + Self::Tagged { tag, literal } => format!("[{tag}:{literal}"), + } + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct EsmtpParameter { pub name: String,