mirror of
https://github.com/mailscope/kumomta.git
synced 2026-10-28 09:16:15 +00:00
cut over smtp server to new command parser
This commit is contained in:
Generated
+1
@@ -849,6 +849,7 @@ dependencies = [
|
||||
"mail-send",
|
||||
"message",
|
||||
"mlua",
|
||||
"rfc5321",
|
||||
"ringbuf",
|
||||
"rustls",
|
||||
"serde",
|
||||
|
||||
@@ -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"]}
|
||||
|
||||
+18
-104
@@ -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<T: AsyncRead + AsyncWrite + Debug + Send + 'static> SmtpServer<T> {
|
||||
// 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<T: AsyncRead + AsyncWrite + Debug + Send + 'static> SmtpServer<T> {
|
||||
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<T: AsyncRead + AsyncWrite + Debug + Send + 'static> SmtpServer<T> {
|
||||
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<T: AsyncRead + AsyncWrite + Debug + Send + 'static> SmtpServer<T> {
|
||||
});
|
||||
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<T: AsyncRead + AsyncWrite + Debug + Send + 'static> SmtpServer<T> {
|
||||
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<Self> {
|
||||
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:<user@example.com>").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:<user@example.com>").unwrap(),
|
||||
Command::Rcpt(EnvelopeAddress::parse("user@example.com").unwrap())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String>,
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user