use std::str::FromStr;
use lazy_static::lazy_static;
use regex::Regex;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Command {
Lhlo(String),
MailFrom(String, Option<u64>),
Recipient(String),
Data,
BinaryData(u64, bool),
Reset,
Verify,
Expand,
Help,
Noop,
Quit,
StartTls,
}
static SIMPLE_COMMANDS: &[(&str, Command, bool)] = &[
("DATA", Command::Data, false),
("RSET", Command::Reset, false),
("VRFY ", Command::Verify, true),
("EXPN ", Command::Expand, true),
("HELP", Command::Help, true),
("NOOP", Command::Noop, false),
("QUIT", Command::Quit, false),
("STARTTLS", Command::StartTls, false),
];
lazy_static! {
static ref RX_LHLO: Regex = Regex::new("^(?i)LHLO ([^ ]*)").unwrap();
static ref RX_MAIL: Regex = Regex::new(
"^(?i)MAIL FROM:<([^>]*)>\
(?: BODY=(?:7BIT|8BIT|BINARYMIME)\
| SIZE=([0-9]+))*$"
)
.unwrap();
static ref RX_RCPT: Regex =
Regex::new("^(?i)RCPT TO:<(?:@[^:]+:)?([^>]+)>$").unwrap();
static ref RX_BDAT: Regex =
Regex::new("^(?i)BDAT ([0-9]+)( LAST)?$").unwrap();
static ref RX_KNOWN_COMMANDS: Regex = Regex::new(
"^(?i)(DATA|RSET|VRFY|EXPN|HELP|NOOP|QUIT|\
STARTTLS|LHLO|MAIL|RCPT|BDAT)( .*)?$"
)
.unwrap();
static ref RX_HELO_EHLO: Regex =
Regex::new("^(?i)(HELO|EHLO)( .*)?$").unwrap();
}
pub fn looks_like_known_command(s: &str) -> bool {
RX_KNOWN_COMMANDS.is_match(s)
}
pub fn looks_like_smtp_helo(s: &str) -> bool {
RX_HELO_EHLO.is_match(s)
}
impl FromStr for Command {
type Err = ();
fn from_str(s: &str) -> Result<Self, ()> {
for &(prefix, ref cmd, allow_trailing_garbage) in SIMPLE_COMMANDS {
if s.len() >= prefix.len()
&& (allow_trailing_garbage || s.len() == prefix.len())
&& s.get(0..prefix.len())
.map_or(false, |sp| prefix.eq_ignore_ascii_case(sp))
{
return Ok(cmd.clone());
}
}
if let Some(cap) = RX_LHLO.captures(s) {
Ok(Command::Lhlo(cap.get(1).unwrap().as_str().to_owned()))
} else if let Some(cap) = RX_MAIL.captures(s) {
let size = match cap.get(2).map(|s| s.as_str().parse::<u64>()) {
None => None,
Some(Err(_)) => return Err(()),
Some(Ok(sz)) => Some(sz),
};
Ok(Command::MailFrom(
cap.get(1).unwrap().as_str().to_owned(),
size,
))
} else if let Some(cap) = RX_RCPT.captures(s) {
Ok(Command::Recipient(cap.get(1).unwrap().as_str().to_owned()))
} else if let Some(cap) = RX_BDAT.captures(s) {
cap.get(1)
.unwrap()
.as_str()
.parse::<u64>()
.map_err(|_| ())
.map(|len| Command::BinaryData(len, cap.get(2).is_some()))
} else {
Err(())
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn command_parsing() {
assert_eq!(
Ok(Command::Lhlo("foo.example.com".to_owned())),
"LHLO foo.example.com".parse()
);
assert_eq!(
Ok(Command::Lhlo("foo.example.com".to_owned())),
"lhlo foo.example.com some client implementation".parse()
);
assert_eq!(
Ok(Command::MailFrom("foo@bar.com".to_owned(), None)),
"MAIL FROM:<foo@bar.com>".parse()
);
assert_eq!(
Ok(Command::MailFrom("foo@bar.com".to_owned(), None)),
"MAIL FROM:<foo@bar.com> BODY=BiNaRyMiMe".parse()
);
assert_eq!(
Ok(Command::MailFrom("foo@bar.com".to_owned(), None)),
"MAIL FROM:<foo@bar.com> body=8bit".parse()
);
assert_eq!(
Ok(Command::MailFrom("foo@bar.com".to_owned(), None)),
"MAIL FROM:<foo@bar.com> body=7bit".parse()
);
assert_eq!(
Ok(Command::MailFrom("foo@bar.com".to_owned(), Some(42))),
"MAIL FROM:<foo@bar.com> SIZE=42".parse()
);
assert_eq!(
Ok(Command::MailFrom("foo@bar.com".to_owned(), Some(42))),
"MAIL FROM:<foo@bar.com> body=7bit size=42".parse()
);
assert_eq!(
Ok(Command::MailFrom("foo@bar.com".to_owned(), Some(42))),
"MAIL FROM:<foo@bar.com> size=42 body=7bit".parse()
);
assert_eq!(
Ok(Command::MailFrom(String::new(), None)),
"mail from:<>".parse()
);
assert_eq!(
Err(()),
"MAIL FROM:<foo@bar.com> size=-1".parse::<Command>()
);
assert_eq!(
Ok(Command::Recipient("userc@d.bar.org".to_owned())),
"RCPT TO:<userc@d.bar.org>".parse()
);
assert_eq!(
Ok(Command::Recipient("userc@d.bar.org".to_owned())),
"rcpt to:<@hosta.int,@jkl.org:userc@d.bar.org>".parse()
);
assert_eq!(Ok(Command::Data), "DATA".parse());
assert_eq!(Ok(Command::Data), "data".parse());
assert_eq!(Err(()), "DATA DATA".parse::<Command>());
assert_eq!(Err(()), "DATABASE".parse::<Command>());
assert_eq!(Ok(Command::BinaryData(42, false)), "BDAT 42".parse());
assert_eq!(
Ok(Command::BinaryData(1000, true)),
"BDAT 1000 LAST".parse()
);
assert_eq!(Ok(Command::BinaryData(1, true)), "bdat 1 last".parse());
assert_eq!(Ok(Command::Reset), "RSET".parse());
assert_eq!(Err(()), "RSET FOO".parse::<Command>());
assert_eq!(Ok(Command::Verify), "VRFY Smith".parse());
assert_eq!(Ok(Command::Verify), "vrfy <foo@bar.com>".parse());
assert_eq!(Err(()), "VRFY".parse::<Command>());
assert_eq!(Ok(Command::Expand), "EXPN Smith".parse());
assert_eq!(Ok(Command::Expand), "EXPN <foo@bar.com>".parse());
assert_eq!(Err(()), "VRFY".parse::<Command>());
assert_eq!(Ok(Command::Help), "HELP".parse());
assert_eq!(Ok(Command::Help), "help me".parse());
assert_eq!(Ok(Command::Noop), "NOOP".parse());
assert_eq!(Err(()), "NOOP NOP".parse::<Command>());
assert_eq!(Ok(Command::Quit), "QUIT".parse());
assert_eq!(Err(()), "QUIT NOW".parse::<Command>());
assert_eq!(Ok(Command::StartTls), "STARTTLS".parse());
assert_eq!(Err(()), "STARTTLS 1.3".parse::<Command>());
}
}