Simplify module structure and public API
This commit is contained in:
parent
351990b95e
commit
0f74c4686d
|
@ -1,6 +1,6 @@
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
use instant_smtp::types::Command;
|
use instant_smtp::Command;
|
||||||
|
|
||||||
fn main() -> std::io::Result<()> {
|
fn main() -> std::io::Result<()> {
|
||||||
let mut args = std::env::args();
|
let mut args = std::env::args();
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
#![no_main]
|
#![no_main]
|
||||||
use libfuzzer_sys::fuzz_target;
|
use libfuzzer_sys::fuzz_target;
|
||||||
use instant_smtp::types::Command;
|
use instant_smtp::Command;
|
||||||
|
|
||||||
fuzz_target!(|data: &[u8]| {
|
fuzz_target!(|data: &[u8]| {
|
||||||
if let Ok((_, cmd)) = Command::from_bytes(data) {
|
if let Ok((_, cmd)) = Command::from_bytes(data) {
|
||||||
|
|
969
src/lib.rs
969
src/lib.rs
|
@ -1,2 +1,969 @@
|
||||||
|
use std::{borrow::Cow, fmt, io::Write, ops::Deref};
|
||||||
|
|
||||||
|
use nom::IResult;
|
||||||
|
#[cfg(feature = "serde")]
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
mod parse;
|
mod parse;
|
||||||
pub mod types;
|
use parse::escape_quoted;
|
||||||
|
use parse::response::is_text_string_byte;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum Command {
|
||||||
|
Ehlo {
|
||||||
|
domain_or_address: DomainOrAddress,
|
||||||
|
},
|
||||||
|
Helo {
|
||||||
|
domain_or_address: DomainOrAddress,
|
||||||
|
},
|
||||||
|
Mail {
|
||||||
|
reverse_path: String,
|
||||||
|
parameters: Vec<Parameter>,
|
||||||
|
},
|
||||||
|
Rcpt {
|
||||||
|
forward_path: String,
|
||||||
|
parameters: Vec<Parameter>,
|
||||||
|
},
|
||||||
|
Data,
|
||||||
|
Rset,
|
||||||
|
/// This command asks the receiver to confirm that the argument
|
||||||
|
/// identifies a user or mailbox. If it is a user name, information is
|
||||||
|
/// returned as specified in Section 3.5.
|
||||||
|
///
|
||||||
|
/// This command has no effect on the reverse-path buffer, the forward-
|
||||||
|
/// path buffer, or the mail data buffer.
|
||||||
|
Vrfy {
|
||||||
|
user_or_mailbox: AtomOrQuoted,
|
||||||
|
},
|
||||||
|
/// This command asks the receiver to confirm that the argument
|
||||||
|
/// identifies a mailing list, and if so, to return the membership of
|
||||||
|
/// that list. If the command is successful, a reply is returned
|
||||||
|
/// containing information as described in Section 3.5. This reply will
|
||||||
|
/// have multiple lines except in the trivial case of a one-member list.
|
||||||
|
///
|
||||||
|
/// This command has no effect on the reverse-path buffer, the forward-
|
||||||
|
/// path buffer, or the mail data buffer, and it may be issued at any
|
||||||
|
/// time.
|
||||||
|
Expn {
|
||||||
|
mailing_list: AtomOrQuoted,
|
||||||
|
},
|
||||||
|
/// This command causes the server to send helpful information to the
|
||||||
|
/// client. The command MAY take an argument (e.g., any command name)
|
||||||
|
/// and return more specific information as a response.
|
||||||
|
///
|
||||||
|
/// SMTP servers SHOULD support HELP without arguments and MAY support it
|
||||||
|
/// with arguments.
|
||||||
|
///
|
||||||
|
/// This command has no effect on the reverse-path buffer, the forward-
|
||||||
|
/// path buffer, or the mail data buffer, and it may be issued at any
|
||||||
|
/// time.
|
||||||
|
Help {
|
||||||
|
argument: Option<AtomOrQuoted>,
|
||||||
|
},
|
||||||
|
/// This command does not affect any parameters or previously entered
|
||||||
|
/// commands. It specifies no action other than that the receiver send a
|
||||||
|
/// "250 OK" reply.
|
||||||
|
///
|
||||||
|
/// If a parameter string is specified, servers SHOULD ignore it.
|
||||||
|
///
|
||||||
|
/// This command has no effect on the reverse-path buffer, the forward-
|
||||||
|
/// path buffer, or the mail data buffer, and it may be issued at any
|
||||||
|
/// time.
|
||||||
|
Noop {
|
||||||
|
argument: Option<AtomOrQuoted>,
|
||||||
|
},
|
||||||
|
/// This command specifies that the receiver MUST send a "221 OK" reply,
|
||||||
|
/// and then close the transmission channel.
|
||||||
|
///
|
||||||
|
/// The receiver MUST NOT intentionally close the transmission channel
|
||||||
|
/// until it receives and replies to a QUIT command (even if there was an
|
||||||
|
/// error). The sender MUST NOT intentionally close the transmission
|
||||||
|
/// channel until it sends a QUIT command, and it SHOULD wait until it
|
||||||
|
/// receives the reply (even if there was an error response to a previous
|
||||||
|
/// command). If the connection is closed prematurely due to violations
|
||||||
|
/// of the above or system or network failure, the server MUST cancel any
|
||||||
|
/// pending transaction, but not undo any previously completed
|
||||||
|
/// transaction, and generally MUST act as if the command or transaction
|
||||||
|
/// in progress had received a temporary error (i.e., a 4yz response).
|
||||||
|
///
|
||||||
|
/// The QUIT command may be issued at any time. Any current uncompleted
|
||||||
|
/// mail transaction will be aborted.
|
||||||
|
Quit,
|
||||||
|
// Extensions
|
||||||
|
StartTls,
|
||||||
|
// AUTH LOGIN
|
||||||
|
AuthLogin(Option<String>),
|
||||||
|
// AUTH PLAIN
|
||||||
|
AuthPlain(Option<String>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Command {
|
||||||
|
pub fn from_bytes(input: &[u8]) -> IResult<&[u8], Self> {
|
||||||
|
crate::parse::command::command(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum DomainOrAddress {
|
||||||
|
Domain(String),
|
||||||
|
Address(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DomainOrAddress {
|
||||||
|
pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
|
||||||
|
match self {
|
||||||
|
DomainOrAddress::Domain(domain) => write!(writer, "{}", domain),
|
||||||
|
DomainOrAddress::Address(address) => write!(writer, "[{}]", address),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum Parameter {
|
||||||
|
/// Message size declaration [RFC1870]
|
||||||
|
Size(u32),
|
||||||
|
Other {
|
||||||
|
keyword: String,
|
||||||
|
value: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum AtomOrQuoted {
|
||||||
|
Atom(String),
|
||||||
|
Quoted(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Command {
|
||||||
|
pub fn name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Command::Ehlo { .. } => "EHLO",
|
||||||
|
Command::Helo { .. } => "HELO",
|
||||||
|
Command::Mail { .. } => "MAIL",
|
||||||
|
Command::Rcpt { .. } => "RCPT",
|
||||||
|
Command::Data => "DATA",
|
||||||
|
Command::Rset => "RSET",
|
||||||
|
Command::Vrfy { .. } => "VRFY",
|
||||||
|
Command::Expn { .. } => "EXPN",
|
||||||
|
Command::Help { .. } => "HELP",
|
||||||
|
Command::Noop { .. } => "NOOP",
|
||||||
|
Command::Quit => "QUIT",
|
||||||
|
// Extensions
|
||||||
|
Command::StartTls => "STARTTLS",
|
||||||
|
// TODO: SMTP AUTH LOGIN
|
||||||
|
Command::AuthLogin(_) => "AUTHLOGIN",
|
||||||
|
// TODO: SMTP AUTH PLAIN
|
||||||
|
Command::AuthPlain(_) => "AUTHPLAIN",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
|
||||||
|
use Command::*;
|
||||||
|
|
||||||
|
match self {
|
||||||
|
// helo = "HELO" SP Domain CRLF
|
||||||
|
Helo { domain_or_address } => {
|
||||||
|
writer.write_all(b"HELO ")?;
|
||||||
|
domain_or_address.serialize(writer)?;
|
||||||
|
}
|
||||||
|
// ehlo = "EHLO" SP ( Domain / address-literal ) CRLF
|
||||||
|
Ehlo { domain_or_address } => {
|
||||||
|
writer.write_all(b"EHLO ")?;
|
||||||
|
domain_or_address.serialize(writer)?;
|
||||||
|
}
|
||||||
|
// mail = "MAIL FROM:" Reverse-path [SP Mail-parameters] CRLF
|
||||||
|
Mail {
|
||||||
|
reverse_path,
|
||||||
|
parameters,
|
||||||
|
} => {
|
||||||
|
writer.write_all(b"MAIL FROM:<")?;
|
||||||
|
writer.write_all(reverse_path.as_bytes())?;
|
||||||
|
writer.write_all(b">")?;
|
||||||
|
|
||||||
|
for parameter in parameters {
|
||||||
|
writer.write_all(b" ")?;
|
||||||
|
parameter.serialize(writer)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// rcpt = "RCPT TO:" ( "<Postmaster@" Domain ">" / "<Postmaster>" / Forward-path ) [SP Rcpt-parameters] CRLF
|
||||||
|
Rcpt {
|
||||||
|
forward_path,
|
||||||
|
parameters,
|
||||||
|
} => {
|
||||||
|
writer.write_all(b"RCPT TO:<")?;
|
||||||
|
writer.write_all(forward_path.as_bytes())?;
|
||||||
|
writer.write_all(b">")?;
|
||||||
|
|
||||||
|
for parameter in parameters {
|
||||||
|
writer.write_all(b" ")?;
|
||||||
|
parameter.serialize(writer)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// data = "DATA" CRLF
|
||||||
|
Data => writer.write_all(b"DATA")?,
|
||||||
|
// rset = "RSET" CRLF
|
||||||
|
Rset => writer.write_all(b"RSET")?,
|
||||||
|
// vrfy = "VRFY" SP String CRLF
|
||||||
|
Vrfy { user_or_mailbox } => {
|
||||||
|
writer.write_all(b"VRFY ")?;
|
||||||
|
user_or_mailbox.serialize(writer)?;
|
||||||
|
}
|
||||||
|
// expn = "EXPN" SP String CRLF
|
||||||
|
Expn { mailing_list } => {
|
||||||
|
writer.write_all(b"EXPN ")?;
|
||||||
|
mailing_list.serialize(writer)?;
|
||||||
|
}
|
||||||
|
// help = "HELP" [ SP String ] CRLF
|
||||||
|
Help { argument: None } => writer.write_all(b"HELP")?,
|
||||||
|
Help {
|
||||||
|
argument: Some(data),
|
||||||
|
} => {
|
||||||
|
writer.write_all(b"HELP ")?;
|
||||||
|
data.serialize(writer)?;
|
||||||
|
}
|
||||||
|
// noop = "NOOP" [ SP String ] CRLF
|
||||||
|
Noop { argument: None } => writer.write_all(b"NOOP")?,
|
||||||
|
Noop {
|
||||||
|
argument: Some(data),
|
||||||
|
} => {
|
||||||
|
writer.write_all(b"NOOP ")?;
|
||||||
|
data.serialize(writer)?;
|
||||||
|
}
|
||||||
|
// quit = "QUIT" CRLF
|
||||||
|
Quit => writer.write_all(b"QUIT")?,
|
||||||
|
// ----- Extensions -----
|
||||||
|
// starttls = "STARTTLS" CRLF
|
||||||
|
StartTls => writer.write_all(b"STARTTLS")?,
|
||||||
|
// auth_login_command = "AUTH LOGIN" [SP username] CRLF
|
||||||
|
AuthLogin(None) => {
|
||||||
|
writer.write_all(b"AUTH LOGIN")?;
|
||||||
|
}
|
||||||
|
AuthLogin(Some(data)) => {
|
||||||
|
writer.write_all(b"AUTH LOGIN ")?;
|
||||||
|
writer.write_all(data.as_bytes())?;
|
||||||
|
}
|
||||||
|
// auth_plain_command = "AUTH PLAIN" [SP base64] CRLF
|
||||||
|
AuthPlain(None) => {
|
||||||
|
writer.write_all(b"AUTH PLAIN")?;
|
||||||
|
}
|
||||||
|
AuthPlain(Some(data)) => {
|
||||||
|
writer.write_all(b"AUTH PLAIN ")?;
|
||||||
|
writer.write_all(data.as_bytes())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(writer, "\r\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parameter {
|
||||||
|
pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
|
||||||
|
match self {
|
||||||
|
Parameter::Size(size) => {
|
||||||
|
write!(writer, "SIZE={}", size)?;
|
||||||
|
}
|
||||||
|
Parameter::Other { keyword, value } => {
|
||||||
|
writer.write_all(keyword.as_bytes())?;
|
||||||
|
|
||||||
|
if let Some(ref value) = value {
|
||||||
|
writer.write_all(b"=")?;
|
||||||
|
writer.write_all(value.as_bytes())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AtomOrQuoted {
|
||||||
|
pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
|
||||||
|
match self {
|
||||||
|
AtomOrQuoted::Atom(atom) => {
|
||||||
|
writer.write_all(atom.as_bytes())?;
|
||||||
|
}
|
||||||
|
AtomOrQuoted::Quoted(quoted) => {
|
||||||
|
writer.write_all(b"\"")?;
|
||||||
|
writer.write_all(escape_quoted(quoted).as_bytes())?;
|
||||||
|
writer.write_all(b"\"")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum Response {
|
||||||
|
Greeting {
|
||||||
|
domain: String,
|
||||||
|
text: String,
|
||||||
|
},
|
||||||
|
Ehlo {
|
||||||
|
domain: String,
|
||||||
|
greet: Option<String>,
|
||||||
|
capabilities: Vec<Capability>,
|
||||||
|
},
|
||||||
|
Other {
|
||||||
|
code: ReplyCode,
|
||||||
|
lines: Vec<TextString<'static>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Response {
|
||||||
|
pub fn parse_greeting(input: &[u8]) -> IResult<&[u8], Self> {
|
||||||
|
crate::parse::response::greeting(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn greeting<D, T>(domain: D, text: T) -> Response
|
||||||
|
where
|
||||||
|
D: Into<String>,
|
||||||
|
T: Into<String>,
|
||||||
|
{
|
||||||
|
Response::Greeting {
|
||||||
|
domain: domain.into(),
|
||||||
|
text: text.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_ehlo(input: &[u8]) -> IResult<&[u8], Self> {
|
||||||
|
crate::parse::response::ehlo_ok_rsp(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_other(input: &[u8]) -> IResult<&[u8], Self> {
|
||||||
|
crate::parse::response::reply_lines(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ehlo<D, G>(domain: D, greet: Option<G>, capabilities: Vec<Capability>) -> Response
|
||||||
|
where
|
||||||
|
D: Into<String>,
|
||||||
|
G: Into<String>,
|
||||||
|
{
|
||||||
|
Response::Ehlo {
|
||||||
|
domain: domain.into(),
|
||||||
|
greet: greet.map(Into::into),
|
||||||
|
capabilities,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn other<T>(code: ReplyCode, text: TextString<'static>) -> Response
|
||||||
|
where
|
||||||
|
T: Into<String>,
|
||||||
|
{
|
||||||
|
Response::Other {
|
||||||
|
code,
|
||||||
|
lines: vec![text],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
|
||||||
|
match self {
|
||||||
|
Response::Greeting { domain, text } => {
|
||||||
|
let lines = text.lines().collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if let Some((first, tail)) = lines.split_first() {
|
||||||
|
if let Some((last, head)) = tail.split_last() {
|
||||||
|
write!(writer, "220-{} {}\r\n", domain, first)?;
|
||||||
|
|
||||||
|
for line in head {
|
||||||
|
write!(writer, "220-{}\r\n", line)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(writer, "220 {}\r\n", last)?;
|
||||||
|
} else {
|
||||||
|
write!(writer, "220 {} {}\r\n", domain, first)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
write!(writer, "220 {}\r\n", domain)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Response::Ehlo {
|
||||||
|
domain,
|
||||||
|
greet,
|
||||||
|
capabilities,
|
||||||
|
} => {
|
||||||
|
let greet = match greet {
|
||||||
|
Some(greet) => format!(" {}", greet),
|
||||||
|
None => "".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some((tail, head)) = capabilities.split_last() {
|
||||||
|
writer.write_all(format!("250-{}{}\r\n", domain, greet).as_bytes())?;
|
||||||
|
|
||||||
|
for capability in head {
|
||||||
|
writer.write_all(b"250-")?;
|
||||||
|
capability.serialize(writer)?;
|
||||||
|
writer.write_all(b"\r\n")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.write_all(b"250 ")?;
|
||||||
|
tail.serialize(writer)?;
|
||||||
|
writer.write_all(b"\r\n")?;
|
||||||
|
} else {
|
||||||
|
writer.write_all(format!("250 {}{}\r\n", domain, greet).as_bytes())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Response::Other { code, lines } => {
|
||||||
|
let code = u16::from(*code);
|
||||||
|
for line in lines.iter().take(lines.len().saturating_sub(1)) {
|
||||||
|
write!(writer, "{}-{}\r\n", code, line,)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
match lines.last() {
|
||||||
|
Some(s) => write!(writer, "{} {}\r\n", code, s)?,
|
||||||
|
None => write!(writer, "{}\r\n", code)?,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum Capability {
|
||||||
|
// Send as mail [RFC821]
|
||||||
|
// The description of SEND was updated by [RFC1123] and then its actual use was deprecated in [RFC2821]
|
||||||
|
// SEND,
|
||||||
|
|
||||||
|
// Send as mail or to terminal [RFC821]
|
||||||
|
// The description of SOML was updated by [RFC1123] and then its actual use was deprecated in [RFC2821]
|
||||||
|
// SOML,
|
||||||
|
|
||||||
|
// Send as mail and to terminal [RFC821]
|
||||||
|
// The description of SAML was updated by [RFC1123] and then its actual use was deprecated in [RFC2821]
|
||||||
|
// SAML,
|
||||||
|
|
||||||
|
// Interchange the client and server roles [RFC821]
|
||||||
|
// The actual use of TURN was deprecated in [RFC2821]
|
||||||
|
// TURN,
|
||||||
|
|
||||||
|
// SMTP Responsible Submitter [RFC4405]
|
||||||
|
// Deprecated by [https://datatracker.ietf.org/doc/status-change-change-sender-id-to-historic].
|
||||||
|
// SUBMITTER,
|
||||||
|
|
||||||
|
// Internationalized email address [RFC5336]
|
||||||
|
// Experimental; deprecated in [RFC6531].
|
||||||
|
// UTF8SMTP,
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
/// Verbose [Eric Allman]
|
||||||
|
// VERB,
|
||||||
|
|
||||||
|
/// One message transaction only [Eric Allman]
|
||||||
|
// ONEX,
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// Expand the mailing list [RFC821]
|
||||||
|
/// Command description updated by [RFC5321]
|
||||||
|
Expn,
|
||||||
|
/// Supply helpful information [RFC821]
|
||||||
|
/// Command description updated by [RFC5321]
|
||||||
|
Help,
|
||||||
|
|
||||||
|
/// SMTP and Submit transport of 8bit MIME content [RFC6152]
|
||||||
|
EightBitMime,
|
||||||
|
|
||||||
|
/// Message size declaration [RFC1870]
|
||||||
|
Size(u32),
|
||||||
|
|
||||||
|
/// Chunking [RFC3030]
|
||||||
|
Chunking,
|
||||||
|
|
||||||
|
/// Binary MIME [RFC3030]
|
||||||
|
BinaryMime,
|
||||||
|
|
||||||
|
/// Checkpoint/Restart [RFC1845]
|
||||||
|
Checkpoint,
|
||||||
|
|
||||||
|
/// Deliver By [RFC2852]
|
||||||
|
DeliverBy,
|
||||||
|
|
||||||
|
/// Command Pipelining [RFC2920]
|
||||||
|
Pipelining,
|
||||||
|
|
||||||
|
/// Delivery Status Notification [RFC3461]
|
||||||
|
Dsn,
|
||||||
|
|
||||||
|
/// Extended Turn [RFC1985]
|
||||||
|
/// SMTP [RFC5321] only. Not for use on Submit port 587.
|
||||||
|
Etrn,
|
||||||
|
|
||||||
|
/// Enhanced Status Codes [RFC2034]
|
||||||
|
EnhancedStatusCodes,
|
||||||
|
|
||||||
|
/// Start TLS [RFC3207]
|
||||||
|
StartTls,
|
||||||
|
|
||||||
|
/// Notification of no soliciting [RFC3865]
|
||||||
|
// NoSoliciting,
|
||||||
|
|
||||||
|
/// Message Tracking [RFC3885]
|
||||||
|
Mtrk,
|
||||||
|
|
||||||
|
/// Authenticated TURN [RFC2645]
|
||||||
|
/// SMTP [RFC5321] only. Not for use on Submit port 587.
|
||||||
|
Atrn,
|
||||||
|
|
||||||
|
/// Authentication [RFC4954]
|
||||||
|
Auth(Vec<AuthMechanism>),
|
||||||
|
|
||||||
|
/// Remote Content [RFC4468]
|
||||||
|
/// Submit [RFC6409] only. Not for use with SMTP on port 25.
|
||||||
|
Burl,
|
||||||
|
|
||||||
|
/// Future Message Release [RFC4865]
|
||||||
|
// FutureRelease,
|
||||||
|
|
||||||
|
/// Content Conversion Permission [RFC4141]
|
||||||
|
// ConPerm,
|
||||||
|
|
||||||
|
/// Content Conversion Negotiation [RFC4141]
|
||||||
|
// ConNeg,
|
||||||
|
|
||||||
|
/// Internationalized email address [RFC6531]
|
||||||
|
SmtpUtf8,
|
||||||
|
|
||||||
|
/// Priority Message Handling [RFC6710]
|
||||||
|
// MTPRIORITY,
|
||||||
|
|
||||||
|
/// Require Recipient Valid Since [RFC7293]
|
||||||
|
Rrvs,
|
||||||
|
|
||||||
|
/// Require TLS [RFC8689]
|
||||||
|
RequireTls,
|
||||||
|
|
||||||
|
// Observed ...
|
||||||
|
// TIME,
|
||||||
|
// XACK,
|
||||||
|
// VERP,
|
||||||
|
// VRFY,
|
||||||
|
/// Other
|
||||||
|
Other {
|
||||||
|
keyword: String,
|
||||||
|
params: Vec<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Capability {
|
||||||
|
pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
|
||||||
|
match self {
|
||||||
|
Capability::Expn => writer.write_all(b"EXPN"),
|
||||||
|
Capability::Help => writer.write_all(b"HELP"),
|
||||||
|
Capability::EightBitMime => writer.write_all(b"8BITMIME"),
|
||||||
|
Capability::Size(number) => writer.write_all(format!("SIZE {}", number).as_bytes()),
|
||||||
|
Capability::Chunking => writer.write_all(b"CHUNKING"),
|
||||||
|
Capability::BinaryMime => writer.write_all(b"BINARYMIME"),
|
||||||
|
Capability::Checkpoint => writer.write_all(b"CHECKPOINT"),
|
||||||
|
Capability::DeliverBy => writer.write_all(b"DELIVERBY"),
|
||||||
|
Capability::Pipelining => writer.write_all(b"PIPELINING"),
|
||||||
|
Capability::Dsn => writer.write_all(b"DSN"),
|
||||||
|
Capability::Etrn => writer.write_all(b"ETRN"),
|
||||||
|
Capability::EnhancedStatusCodes => writer.write_all(b"ENHANCEDSTATUSCODES"),
|
||||||
|
Capability::StartTls => writer.write_all(b"STARTTLS"),
|
||||||
|
Capability::Mtrk => writer.write_all(b"MTRK"),
|
||||||
|
Capability::Atrn => writer.write_all(b"ATRN"),
|
||||||
|
Capability::Auth(mechanisms) => {
|
||||||
|
if let Some((tail, head)) = mechanisms.split_last() {
|
||||||
|
writer.write_all(b"AUTH ")?;
|
||||||
|
|
||||||
|
for mechanism in head {
|
||||||
|
mechanism.serialize(writer)?;
|
||||||
|
writer.write_all(b" ")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
tail.serialize(writer)
|
||||||
|
} else {
|
||||||
|
writer.write_all(b"AUTH")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Capability::Burl => writer.write_all(b"BURL"),
|
||||||
|
Capability::SmtpUtf8 => writer.write_all(b"SMTPUTF8"),
|
||||||
|
Capability::Rrvs => writer.write_all(b"RRVS"),
|
||||||
|
Capability::RequireTls => writer.write_all(b"REQUIRETLS"),
|
||||||
|
Capability::Other { keyword, params } => {
|
||||||
|
if let Some((tail, head)) = params.split_last() {
|
||||||
|
writer.write_all(keyword.as_bytes())?;
|
||||||
|
writer.write_all(b" ")?;
|
||||||
|
|
||||||
|
for param in head {
|
||||||
|
writer.write_all(param.as_bytes())?;
|
||||||
|
writer.write_all(b" ")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.write_all(tail.as_bytes())
|
||||||
|
} else {
|
||||||
|
writer.write_all(keyword.as_bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
|
||||||
|
pub enum ReplyCode {
|
||||||
|
/// 211 System status, or system help reply
|
||||||
|
SystemStatus,
|
||||||
|
/// 214 Help message
|
||||||
|
///
|
||||||
|
/// Information on how to use the receiver or the meaning of a particular non-standard
|
||||||
|
/// command; this reply is useful only to the human user.
|
||||||
|
HelpMessage,
|
||||||
|
/// 220 <domain> Service ready
|
||||||
|
Ready,
|
||||||
|
/// 221 <domain> Service closing transmission channel
|
||||||
|
ClosingChannel,
|
||||||
|
/// 250 Requested mail action okay, completed
|
||||||
|
Ok,
|
||||||
|
/// 251 User not local; will forward to <forward-path>
|
||||||
|
UserNotLocalWillForward,
|
||||||
|
/// 252 Cannot VRFY user, but will accept message and attempt delivery
|
||||||
|
CannotVrfy,
|
||||||
|
/// 354 Start mail input; end with <CRLF>.<CRLF>
|
||||||
|
StartMailInput,
|
||||||
|
/// 421 <domain> Service not available, closing transmission channel
|
||||||
|
///
|
||||||
|
/// This may be a reply to any command if the service knows it must shut down.
|
||||||
|
NotAvailable,
|
||||||
|
/// 450 Requested mail action not taken: mailbox unavailable
|
||||||
|
///
|
||||||
|
/// E.g., mailbox busy or temporarily blocked for policy reasons.
|
||||||
|
MailboxTemporarilyUnavailable,
|
||||||
|
/// 451 Requested action aborted: local error in processing
|
||||||
|
ProcessingError,
|
||||||
|
/// 452 Requested action not taken: insufficient system storage
|
||||||
|
InsufficientStorage,
|
||||||
|
/// 455 Server unable to accommodate parameters
|
||||||
|
UnableToAccommodateParameters,
|
||||||
|
/// 500 Syntax error, command unrecognized
|
||||||
|
SyntaxError,
|
||||||
|
/// 501 Syntax error in parameters or arguments
|
||||||
|
ParameterSyntaxError,
|
||||||
|
/// 502 Command not implemented
|
||||||
|
CommandNotImplemented,
|
||||||
|
/// 503 Bad sequence of commands
|
||||||
|
BadSequence,
|
||||||
|
/// 504 Command parameter not implemented
|
||||||
|
ParameterNotImplemented,
|
||||||
|
/// 521 <domain> does not accept mail (see RFC 1846)
|
||||||
|
NoMailService,
|
||||||
|
/// 550 Requested action not taken: mailbox unavailable
|
||||||
|
///
|
||||||
|
/// E.g. mailbox not found, no access, or command rejected for policy reasons.
|
||||||
|
MailboxPermanentlyUnavailable,
|
||||||
|
/// 551 User not local; please try <forward-path>
|
||||||
|
UserNotLocal,
|
||||||
|
/// 552 Requested mail action aborted: exceeded storage allocation
|
||||||
|
ExceededStorageAllocation,
|
||||||
|
/// 553 Requested action not taken: mailbox name not allowed
|
||||||
|
///
|
||||||
|
/// E.g. mailbox syntax incorrect.
|
||||||
|
MailboxNameNotAllowed,
|
||||||
|
/// 554 Transaction failed
|
||||||
|
///
|
||||||
|
/// Or, in the case of a connection-opening response, "No SMTP service here".
|
||||||
|
TransactionFailed,
|
||||||
|
/// 555 MAIL FROM/RCPT TO parameters not recognized or not implemented
|
||||||
|
ParametersNotImplemented,
|
||||||
|
/// Miscellaneous reply codes
|
||||||
|
Other(u16),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReplyCode {
|
||||||
|
pub fn is_completed(&self) -> bool {
|
||||||
|
let code = u16::from(*self);
|
||||||
|
code > 199 && code < 300
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_accepted(&self) -> bool {
|
||||||
|
let code = u16::from(*self);
|
||||||
|
code > 299 && code < 400
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_temporary_error(&self) -> bool {
|
||||||
|
let code = u16::from(*self);
|
||||||
|
code > 399 && code < 500
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_permanent_error(&self) -> bool {
|
||||||
|
let code = u16::from(*self);
|
||||||
|
code > 499 && code < 600
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u16> for ReplyCode {
|
||||||
|
fn from(value: u16) -> Self {
|
||||||
|
match value {
|
||||||
|
211 => ReplyCode::SystemStatus,
|
||||||
|
214 => ReplyCode::HelpMessage,
|
||||||
|
220 => ReplyCode::Ready,
|
||||||
|
221 => ReplyCode::ClosingChannel,
|
||||||
|
250 => ReplyCode::Ok,
|
||||||
|
251 => ReplyCode::UserNotLocalWillForward,
|
||||||
|
252 => ReplyCode::CannotVrfy,
|
||||||
|
354 => ReplyCode::StartMailInput,
|
||||||
|
421 => ReplyCode::NotAvailable,
|
||||||
|
450 => ReplyCode::MailboxTemporarilyUnavailable,
|
||||||
|
451 => ReplyCode::ProcessingError,
|
||||||
|
452 => ReplyCode::InsufficientStorage,
|
||||||
|
455 => ReplyCode::UnableToAccommodateParameters,
|
||||||
|
500 => ReplyCode::SyntaxError,
|
||||||
|
501 => ReplyCode::ParameterSyntaxError,
|
||||||
|
502 => ReplyCode::CommandNotImplemented,
|
||||||
|
503 => ReplyCode::BadSequence,
|
||||||
|
504 => ReplyCode::ParameterNotImplemented,
|
||||||
|
521 => ReplyCode::NoMailService,
|
||||||
|
550 => ReplyCode::MailboxPermanentlyUnavailable,
|
||||||
|
551 => ReplyCode::UserNotLocal,
|
||||||
|
552 => ReplyCode::ExceededStorageAllocation,
|
||||||
|
553 => ReplyCode::MailboxNameNotAllowed,
|
||||||
|
554 => ReplyCode::TransactionFailed,
|
||||||
|
555 => ReplyCode::ParametersNotImplemented,
|
||||||
|
_ => ReplyCode::Other(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ReplyCode> for u16 {
|
||||||
|
fn from(value: ReplyCode) -> Self {
|
||||||
|
match value {
|
||||||
|
ReplyCode::SystemStatus => 211,
|
||||||
|
ReplyCode::HelpMessage => 214,
|
||||||
|
ReplyCode::Ready => 220,
|
||||||
|
ReplyCode::ClosingChannel => 221,
|
||||||
|
ReplyCode::Ok => 250,
|
||||||
|
ReplyCode::UserNotLocalWillForward => 251,
|
||||||
|
ReplyCode::CannotVrfy => 252,
|
||||||
|
ReplyCode::StartMailInput => 354,
|
||||||
|
ReplyCode::NotAvailable => 421,
|
||||||
|
ReplyCode::MailboxTemporarilyUnavailable => 450,
|
||||||
|
ReplyCode::ProcessingError => 451,
|
||||||
|
ReplyCode::InsufficientStorage => 452,
|
||||||
|
ReplyCode::UnableToAccommodateParameters => 455,
|
||||||
|
ReplyCode::SyntaxError => 500,
|
||||||
|
ReplyCode::ParameterSyntaxError => 501,
|
||||||
|
ReplyCode::CommandNotImplemented => 502,
|
||||||
|
ReplyCode::BadSequence => 503,
|
||||||
|
ReplyCode::ParameterNotImplemented => 504,
|
||||||
|
ReplyCode::NoMailService => 521,
|
||||||
|
ReplyCode::MailboxPermanentlyUnavailable => 550,
|
||||||
|
ReplyCode::UserNotLocal => 551,
|
||||||
|
ReplyCode::ExceededStorageAllocation => 552,
|
||||||
|
ReplyCode::MailboxNameNotAllowed => 553,
|
||||||
|
ReplyCode::TransactionFailed => 554,
|
||||||
|
ReplyCode::ParametersNotImplemented => 555,
|
||||||
|
ReplyCode::Other(v) => v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum AuthMechanism {
|
||||||
|
Plain,
|
||||||
|
Login,
|
||||||
|
GssApi,
|
||||||
|
|
||||||
|
CramMd5,
|
||||||
|
CramSha1,
|
||||||
|
ScramMd5,
|
||||||
|
DigestMd5,
|
||||||
|
Ntlm,
|
||||||
|
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthMechanism {
|
||||||
|
pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
|
||||||
|
match self {
|
||||||
|
AuthMechanism::Plain => writer.write_all(b"PLAIN"),
|
||||||
|
AuthMechanism::Login => writer.write_all(b"LOGIN"),
|
||||||
|
AuthMechanism::GssApi => writer.write_all(b"GSSAPI"),
|
||||||
|
|
||||||
|
AuthMechanism::CramMd5 => writer.write_all(b"CRAM-MD5"),
|
||||||
|
AuthMechanism::CramSha1 => writer.write_all(b"CRAM-SHA1"),
|
||||||
|
AuthMechanism::ScramMd5 => writer.write_all(b"SCRAM-MD5"),
|
||||||
|
AuthMechanism::DigestMd5 => writer.write_all(b"DIGEST-MD5"),
|
||||||
|
AuthMechanism::Ntlm => writer.write_all(b"NTLM"),
|
||||||
|
|
||||||
|
AuthMechanism::Other(other) => writer.write_all(other.as_bytes()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A string containing of tab, space and printable ASCII characters
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct TextString<'a>(pub(crate) Cow<'a, str>);
|
||||||
|
|
||||||
|
impl<'a> TextString<'a> {
|
||||||
|
pub fn new(s: &'a str) -> Result<Self, InvalidTextString> {
|
||||||
|
match s.as_bytes().iter().all(|&b| is_text_string_byte(b)) {
|
||||||
|
true => Ok(TextString(Cow::Borrowed(s))),
|
||||||
|
false => Err(InvalidTextString(())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_owned(self) -> TextString<'static> {
|
||||||
|
TextString(self.0.into_owned().into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for TextString<'_> {
|
||||||
|
type Target = str;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for TextString<'_> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct InvalidTextString(());
|
||||||
|
|
||||||
|
impl fmt::Display for InvalidTextString {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "input contains invalid characters")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for InvalidTextString {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{Capability, ReplyCode, Response, TextString};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_greeting() {
|
||||||
|
let tests = &[
|
||||||
|
(
|
||||||
|
Response::Greeting {
|
||||||
|
domain: "example.org".into(),
|
||||||
|
text: "".into(),
|
||||||
|
},
|
||||||
|
b"220 example.org\r\n".as_ref(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Response::Greeting {
|
||||||
|
domain: "example.org".into(),
|
||||||
|
text: "A".into(),
|
||||||
|
},
|
||||||
|
b"220 example.org A\r\n".as_ref(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Response::Greeting {
|
||||||
|
domain: "example.org".into(),
|
||||||
|
text: "A\nB".into(),
|
||||||
|
},
|
||||||
|
b"220-example.org A\r\n220 B\r\n".as_ref(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Response::Greeting {
|
||||||
|
domain: "example.org".into(),
|
||||||
|
text: "A\nB\nC".into(),
|
||||||
|
},
|
||||||
|
b"220-example.org A\r\n220-B\r\n220 C\r\n".as_ref(),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (test, expected) in tests.iter() {
|
||||||
|
let mut got = Vec::new();
|
||||||
|
test.serialize(&mut got).unwrap();
|
||||||
|
assert_eq!(expected, &got);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_ehlo() {
|
||||||
|
let tests = &[
|
||||||
|
(
|
||||||
|
Response::Ehlo {
|
||||||
|
domain: "example.org".into(),
|
||||||
|
greet: None,
|
||||||
|
capabilities: vec![],
|
||||||
|
},
|
||||||
|
b"250 example.org\r\n".as_ref(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Response::Ehlo {
|
||||||
|
domain: "example.org".into(),
|
||||||
|
greet: Some("...".into()),
|
||||||
|
capabilities: vec![],
|
||||||
|
},
|
||||||
|
b"250 example.org ...\r\n".as_ref(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Response::Ehlo {
|
||||||
|
domain: "example.org".into(),
|
||||||
|
greet: Some("...".into()),
|
||||||
|
capabilities: vec![Capability::StartTls],
|
||||||
|
},
|
||||||
|
b"250-example.org ...\r\n250 STARTTLS\r\n".as_ref(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Response::Ehlo {
|
||||||
|
domain: "example.org".into(),
|
||||||
|
greet: Some("...".into()),
|
||||||
|
capabilities: vec![Capability::StartTls, Capability::Size(12345)],
|
||||||
|
},
|
||||||
|
b"250-example.org ...\r\n250-STARTTLS\r\n250 SIZE 12345\r\n".as_ref(),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (test, expected) in tests.iter() {
|
||||||
|
let mut got = Vec::new();
|
||||||
|
test.serialize(&mut got).unwrap();
|
||||||
|
assert_eq!(expected, &got);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_serialize_other() {
|
||||||
|
let tests = &[
|
||||||
|
(
|
||||||
|
Response::Other {
|
||||||
|
code: ReplyCode::StartMailInput,
|
||||||
|
lines: vec![],
|
||||||
|
},
|
||||||
|
b"354\r\n".as_ref(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Response::Other {
|
||||||
|
code: ReplyCode::StartMailInput,
|
||||||
|
lines: vec![TextString::new("A").unwrap()],
|
||||||
|
},
|
||||||
|
b"354 A\r\n".as_ref(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Response::Other {
|
||||||
|
code: ReplyCode::StartMailInput,
|
||||||
|
lines: vec![TextString::new("A").unwrap(), TextString::new("B").unwrap()],
|
||||||
|
},
|
||||||
|
b"354-A\r\n354 B\r\n".as_ref(),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (test, expected) in tests.iter() {
|
||||||
|
let mut got = Vec::new();
|
||||||
|
test.serialize(&mut got).unwrap();
|
||||||
|
assert_eq!(expected, &got);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ use nom::{
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
parse::{address::address_literal, atom, base64, domain, quoted_string, string},
|
parse::{address::address_literal, atom, base64, domain, quoted_string, string},
|
||||||
types::{Command, DomainOrAddress, Parameter},
|
{Command, DomainOrAddress, Parameter},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn command(input: &[u8]) -> IResult<&[u8], Command> {
|
pub fn command(input: &[u8]) -> IResult<&[u8], Command> {
|
||||||
|
@ -373,7 +373,7 @@ pub fn dot_string(input: &[u8]) -> IResult<&[u8], &str> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::{ehlo, helo, mail, Parameter};
|
use super::{ehlo, helo, mail, Parameter};
|
||||||
use crate::types::{Command, DomainOrAddress};
|
use crate::{Command, DomainOrAddress};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ehlo() {
|
fn test_ehlo() {
|
||||||
|
|
|
@ -11,7 +11,7 @@ use nom::{
|
||||||
IResult,
|
IResult,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::types::AtomOrQuoted;
|
use super::AtomOrQuoted;
|
||||||
|
|
||||||
pub mod address;
|
pub mod address;
|
||||||
pub mod command;
|
pub mod command;
|
||||||
|
|
|
@ -12,7 +12,7 @@ use nom::{
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
parse::{address::address_literal, domain, number},
|
parse::{address::address_literal, domain, number},
|
||||||
types::{AuthMechanism, Capability, ReplyCode, Response, TextString},
|
{AuthMechanism, Capability, ReplyCode, Response, TextString},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Greeting = ( "220 " (Domain / address-literal) [ SP textstring ] CRLF ) /
|
/// Greeting = ( "220 " (Domain / address-literal) [ SP textstring ] CRLF ) /
|
||||||
|
@ -302,7 +302,7 @@ pub fn auth_mechanism(input: &[u8]) -> IResult<&[u8], AuthMechanism> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::types::AuthMechanism;
|
use crate::AuthMechanism;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_greeting() {
|
fn test_greeting() {
|
||||||
|
|
968
src/types.rs
968
src/types.rs
|
@ -1,968 +0,0 @@
|
||||||
use std::{borrow::Cow, fmt, io::Write, ops::Deref};
|
|
||||||
|
|
||||||
use nom::IResult;
|
|
||||||
#[cfg(feature = "serde")]
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::parse::escape_quoted;
|
|
||||||
use crate::parse::response::is_text_string_byte;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub enum Command {
|
|
||||||
Ehlo {
|
|
||||||
domain_or_address: DomainOrAddress,
|
|
||||||
},
|
|
||||||
Helo {
|
|
||||||
domain_or_address: DomainOrAddress,
|
|
||||||
},
|
|
||||||
Mail {
|
|
||||||
reverse_path: String,
|
|
||||||
parameters: Vec<Parameter>,
|
|
||||||
},
|
|
||||||
Rcpt {
|
|
||||||
forward_path: String,
|
|
||||||
parameters: Vec<Parameter>,
|
|
||||||
},
|
|
||||||
Data,
|
|
||||||
Rset,
|
|
||||||
/// This command asks the receiver to confirm that the argument
|
|
||||||
/// identifies a user or mailbox. If it is a user name, information is
|
|
||||||
/// returned as specified in Section 3.5.
|
|
||||||
///
|
|
||||||
/// This command has no effect on the reverse-path buffer, the forward-
|
|
||||||
/// path buffer, or the mail data buffer.
|
|
||||||
Vrfy {
|
|
||||||
user_or_mailbox: AtomOrQuoted,
|
|
||||||
},
|
|
||||||
/// This command asks the receiver to confirm that the argument
|
|
||||||
/// identifies a mailing list, and if so, to return the membership of
|
|
||||||
/// that list. If the command is successful, a reply is returned
|
|
||||||
/// containing information as described in Section 3.5. This reply will
|
|
||||||
/// have multiple lines except in the trivial case of a one-member list.
|
|
||||||
///
|
|
||||||
/// This command has no effect on the reverse-path buffer, the forward-
|
|
||||||
/// path buffer, or the mail data buffer, and it may be issued at any
|
|
||||||
/// time.
|
|
||||||
Expn {
|
|
||||||
mailing_list: AtomOrQuoted,
|
|
||||||
},
|
|
||||||
/// This command causes the server to send helpful information to the
|
|
||||||
/// client. The command MAY take an argument (e.g., any command name)
|
|
||||||
/// and return more specific information as a response.
|
|
||||||
///
|
|
||||||
/// SMTP servers SHOULD support HELP without arguments and MAY support it
|
|
||||||
/// with arguments.
|
|
||||||
///
|
|
||||||
/// This command has no effect on the reverse-path buffer, the forward-
|
|
||||||
/// path buffer, or the mail data buffer, and it may be issued at any
|
|
||||||
/// time.
|
|
||||||
Help {
|
|
||||||
argument: Option<AtomOrQuoted>,
|
|
||||||
},
|
|
||||||
/// This command does not affect any parameters or previously entered
|
|
||||||
/// commands. It specifies no action other than that the receiver send a
|
|
||||||
/// "250 OK" reply.
|
|
||||||
///
|
|
||||||
/// If a parameter string is specified, servers SHOULD ignore it.
|
|
||||||
///
|
|
||||||
/// This command has no effect on the reverse-path buffer, the forward-
|
|
||||||
/// path buffer, or the mail data buffer, and it may be issued at any
|
|
||||||
/// time.
|
|
||||||
Noop {
|
|
||||||
argument: Option<AtomOrQuoted>,
|
|
||||||
},
|
|
||||||
/// This command specifies that the receiver MUST send a "221 OK" reply,
|
|
||||||
/// and then close the transmission channel.
|
|
||||||
///
|
|
||||||
/// The receiver MUST NOT intentionally close the transmission channel
|
|
||||||
/// until it receives and replies to a QUIT command (even if there was an
|
|
||||||
/// error). The sender MUST NOT intentionally close the transmission
|
|
||||||
/// channel until it sends a QUIT command, and it SHOULD wait until it
|
|
||||||
/// receives the reply (even if there was an error response to a previous
|
|
||||||
/// command). If the connection is closed prematurely due to violations
|
|
||||||
/// of the above or system or network failure, the server MUST cancel any
|
|
||||||
/// pending transaction, but not undo any previously completed
|
|
||||||
/// transaction, and generally MUST act as if the command or transaction
|
|
||||||
/// in progress had received a temporary error (i.e., a 4yz response).
|
|
||||||
///
|
|
||||||
/// The QUIT command may be issued at any time. Any current uncompleted
|
|
||||||
/// mail transaction will be aborted.
|
|
||||||
Quit,
|
|
||||||
// Extensions
|
|
||||||
StartTls,
|
|
||||||
// AUTH LOGIN
|
|
||||||
AuthLogin(Option<String>),
|
|
||||||
// AUTH PLAIN
|
|
||||||
AuthPlain(Option<String>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Command {
|
|
||||||
pub fn from_bytes(input: &[u8]) -> IResult<&[u8], Self> {
|
|
||||||
crate::parse::command::command(input)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub enum DomainOrAddress {
|
|
||||||
Domain(String),
|
|
||||||
Address(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DomainOrAddress {
|
|
||||||
pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
|
|
||||||
match self {
|
|
||||||
DomainOrAddress::Domain(domain) => write!(writer, "{}", domain),
|
|
||||||
DomainOrAddress::Address(address) => write!(writer, "[{}]", address),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum Parameter {
|
|
||||||
/// Message size declaration [RFC1870]
|
|
||||||
Size(u32),
|
|
||||||
Other {
|
|
||||||
keyword: String,
|
|
||||||
value: Option<String>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum AtomOrQuoted {
|
|
||||||
Atom(String),
|
|
||||||
Quoted(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Command {
|
|
||||||
pub fn name(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Command::Ehlo { .. } => "EHLO",
|
|
||||||
Command::Helo { .. } => "HELO",
|
|
||||||
Command::Mail { .. } => "MAIL",
|
|
||||||
Command::Rcpt { .. } => "RCPT",
|
|
||||||
Command::Data => "DATA",
|
|
||||||
Command::Rset => "RSET",
|
|
||||||
Command::Vrfy { .. } => "VRFY",
|
|
||||||
Command::Expn { .. } => "EXPN",
|
|
||||||
Command::Help { .. } => "HELP",
|
|
||||||
Command::Noop { .. } => "NOOP",
|
|
||||||
Command::Quit => "QUIT",
|
|
||||||
// Extensions
|
|
||||||
Command::StartTls => "STARTTLS",
|
|
||||||
// TODO: SMTP AUTH LOGIN
|
|
||||||
Command::AuthLogin(_) => "AUTHLOGIN",
|
|
||||||
// TODO: SMTP AUTH PLAIN
|
|
||||||
Command::AuthPlain(_) => "AUTHPLAIN",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
|
|
||||||
use Command::*;
|
|
||||||
|
|
||||||
match self {
|
|
||||||
// helo = "HELO" SP Domain CRLF
|
|
||||||
Helo { domain_or_address } => {
|
|
||||||
writer.write_all(b"HELO ")?;
|
|
||||||
domain_or_address.serialize(writer)?;
|
|
||||||
}
|
|
||||||
// ehlo = "EHLO" SP ( Domain / address-literal ) CRLF
|
|
||||||
Ehlo { domain_or_address } => {
|
|
||||||
writer.write_all(b"EHLO ")?;
|
|
||||||
domain_or_address.serialize(writer)?;
|
|
||||||
}
|
|
||||||
// mail = "MAIL FROM:" Reverse-path [SP Mail-parameters] CRLF
|
|
||||||
Mail {
|
|
||||||
reverse_path,
|
|
||||||
parameters,
|
|
||||||
} => {
|
|
||||||
writer.write_all(b"MAIL FROM:<")?;
|
|
||||||
writer.write_all(reverse_path.as_bytes())?;
|
|
||||||
writer.write_all(b">")?;
|
|
||||||
|
|
||||||
for parameter in parameters {
|
|
||||||
writer.write_all(b" ")?;
|
|
||||||
parameter.serialize(writer)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// rcpt = "RCPT TO:" ( "<Postmaster@" Domain ">" / "<Postmaster>" / Forward-path ) [SP Rcpt-parameters] CRLF
|
|
||||||
Rcpt {
|
|
||||||
forward_path,
|
|
||||||
parameters,
|
|
||||||
} => {
|
|
||||||
writer.write_all(b"RCPT TO:<")?;
|
|
||||||
writer.write_all(forward_path.as_bytes())?;
|
|
||||||
writer.write_all(b">")?;
|
|
||||||
|
|
||||||
for parameter in parameters {
|
|
||||||
writer.write_all(b" ")?;
|
|
||||||
parameter.serialize(writer)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// data = "DATA" CRLF
|
|
||||||
Data => writer.write_all(b"DATA")?,
|
|
||||||
// rset = "RSET" CRLF
|
|
||||||
Rset => writer.write_all(b"RSET")?,
|
|
||||||
// vrfy = "VRFY" SP String CRLF
|
|
||||||
Vrfy { user_or_mailbox } => {
|
|
||||||
writer.write_all(b"VRFY ")?;
|
|
||||||
user_or_mailbox.serialize(writer)?;
|
|
||||||
}
|
|
||||||
// expn = "EXPN" SP String CRLF
|
|
||||||
Expn { mailing_list } => {
|
|
||||||
writer.write_all(b"EXPN ")?;
|
|
||||||
mailing_list.serialize(writer)?;
|
|
||||||
}
|
|
||||||
// help = "HELP" [ SP String ] CRLF
|
|
||||||
Help { argument: None } => writer.write_all(b"HELP")?,
|
|
||||||
Help {
|
|
||||||
argument: Some(data),
|
|
||||||
} => {
|
|
||||||
writer.write_all(b"HELP ")?;
|
|
||||||
data.serialize(writer)?;
|
|
||||||
}
|
|
||||||
// noop = "NOOP" [ SP String ] CRLF
|
|
||||||
Noop { argument: None } => writer.write_all(b"NOOP")?,
|
|
||||||
Noop {
|
|
||||||
argument: Some(data),
|
|
||||||
} => {
|
|
||||||
writer.write_all(b"NOOP ")?;
|
|
||||||
data.serialize(writer)?;
|
|
||||||
}
|
|
||||||
// quit = "QUIT" CRLF
|
|
||||||
Quit => writer.write_all(b"QUIT")?,
|
|
||||||
// ----- Extensions -----
|
|
||||||
// starttls = "STARTTLS" CRLF
|
|
||||||
StartTls => writer.write_all(b"STARTTLS")?,
|
|
||||||
// auth_login_command = "AUTH LOGIN" [SP username] CRLF
|
|
||||||
AuthLogin(None) => {
|
|
||||||
writer.write_all(b"AUTH LOGIN")?;
|
|
||||||
}
|
|
||||||
AuthLogin(Some(data)) => {
|
|
||||||
writer.write_all(b"AUTH LOGIN ")?;
|
|
||||||
writer.write_all(data.as_bytes())?;
|
|
||||||
}
|
|
||||||
// auth_plain_command = "AUTH PLAIN" [SP base64] CRLF
|
|
||||||
AuthPlain(None) => {
|
|
||||||
writer.write_all(b"AUTH PLAIN")?;
|
|
||||||
}
|
|
||||||
AuthPlain(Some(data)) => {
|
|
||||||
writer.write_all(b"AUTH PLAIN ")?;
|
|
||||||
writer.write_all(data.as_bytes())?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
write!(writer, "\r\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Parameter {
|
|
||||||
pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
|
|
||||||
match self {
|
|
||||||
Parameter::Size(size) => {
|
|
||||||
write!(writer, "SIZE={}", size)?;
|
|
||||||
}
|
|
||||||
Parameter::Other { keyword, value } => {
|
|
||||||
writer.write_all(keyword.as_bytes())?;
|
|
||||||
|
|
||||||
if let Some(ref value) = value {
|
|
||||||
writer.write_all(b"=")?;
|
|
||||||
writer.write_all(value.as_bytes())?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AtomOrQuoted {
|
|
||||||
pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
|
|
||||||
match self {
|
|
||||||
AtomOrQuoted::Atom(atom) => {
|
|
||||||
writer.write_all(atom.as_bytes())?;
|
|
||||||
}
|
|
||||||
AtomOrQuoted::Quoted(quoted) => {
|
|
||||||
writer.write_all(b"\"")?;
|
|
||||||
writer.write_all(escape_quoted(quoted).as_bytes())?;
|
|
||||||
writer.write_all(b"\"")?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum Response {
|
|
||||||
Greeting {
|
|
||||||
domain: String,
|
|
||||||
text: String,
|
|
||||||
},
|
|
||||||
Ehlo {
|
|
||||||
domain: String,
|
|
||||||
greet: Option<String>,
|
|
||||||
capabilities: Vec<Capability>,
|
|
||||||
},
|
|
||||||
Other {
|
|
||||||
code: ReplyCode,
|
|
||||||
lines: Vec<TextString<'static>>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Response {
|
|
||||||
pub fn parse_greeting(input: &[u8]) -> IResult<&[u8], Self> {
|
|
||||||
crate::parse::response::greeting(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn greeting<D, T>(domain: D, text: T) -> Response
|
|
||||||
where
|
|
||||||
D: Into<String>,
|
|
||||||
T: Into<String>,
|
|
||||||
{
|
|
||||||
Response::Greeting {
|
|
||||||
domain: domain.into(),
|
|
||||||
text: text.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_ehlo(input: &[u8]) -> IResult<&[u8], Self> {
|
|
||||||
crate::parse::response::ehlo_ok_rsp(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_other(input: &[u8]) -> IResult<&[u8], Self> {
|
|
||||||
crate::parse::response::reply_lines(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ehlo<D, G>(domain: D, greet: Option<G>, capabilities: Vec<Capability>) -> Response
|
|
||||||
where
|
|
||||||
D: Into<String>,
|
|
||||||
G: Into<String>,
|
|
||||||
{
|
|
||||||
Response::Ehlo {
|
|
||||||
domain: domain.into(),
|
|
||||||
greet: greet.map(Into::into),
|
|
||||||
capabilities,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn other<T>(code: ReplyCode, text: TextString<'static>) -> Response
|
|
||||||
where
|
|
||||||
T: Into<String>,
|
|
||||||
{
|
|
||||||
Response::Other {
|
|
||||||
code,
|
|
||||||
lines: vec![text],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
|
|
||||||
match self {
|
|
||||||
Response::Greeting { domain, text } => {
|
|
||||||
let lines = text.lines().collect::<Vec<_>>();
|
|
||||||
|
|
||||||
if let Some((first, tail)) = lines.split_first() {
|
|
||||||
if let Some((last, head)) = tail.split_last() {
|
|
||||||
write!(writer, "220-{} {}\r\n", domain, first)?;
|
|
||||||
|
|
||||||
for line in head {
|
|
||||||
write!(writer, "220-{}\r\n", line)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
write!(writer, "220 {}\r\n", last)?;
|
|
||||||
} else {
|
|
||||||
write!(writer, "220 {} {}\r\n", domain, first)?;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
write!(writer, "220 {}\r\n", domain)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Response::Ehlo {
|
|
||||||
domain,
|
|
||||||
greet,
|
|
||||||
capabilities,
|
|
||||||
} => {
|
|
||||||
let greet = match greet {
|
|
||||||
Some(greet) => format!(" {}", greet),
|
|
||||||
None => "".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some((tail, head)) = capabilities.split_last() {
|
|
||||||
writer.write_all(format!("250-{}{}\r\n", domain, greet).as_bytes())?;
|
|
||||||
|
|
||||||
for capability in head {
|
|
||||||
writer.write_all(b"250-")?;
|
|
||||||
capability.serialize(writer)?;
|
|
||||||
writer.write_all(b"\r\n")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.write_all(b"250 ")?;
|
|
||||||
tail.serialize(writer)?;
|
|
||||||
writer.write_all(b"\r\n")?;
|
|
||||||
} else {
|
|
||||||
writer.write_all(format!("250 {}{}\r\n", domain, greet).as_bytes())?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Response::Other { code, lines } => {
|
|
||||||
let code = u16::from(*code);
|
|
||||||
for line in lines.iter().take(lines.len().saturating_sub(1)) {
|
|
||||||
write!(writer, "{}-{}\r\n", code, line,)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
match lines.last() {
|
|
||||||
Some(s) => write!(writer, "{} {}\r\n", code, s)?,
|
|
||||||
None => write!(writer, "{}\r\n", code)?,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum Capability {
|
|
||||||
// Send as mail [RFC821]
|
|
||||||
// The description of SEND was updated by [RFC1123] and then its actual use was deprecated in [RFC2821]
|
|
||||||
// SEND,
|
|
||||||
|
|
||||||
// Send as mail or to terminal [RFC821]
|
|
||||||
// The description of SOML was updated by [RFC1123] and then its actual use was deprecated in [RFC2821]
|
|
||||||
// SOML,
|
|
||||||
|
|
||||||
// Send as mail and to terminal [RFC821]
|
|
||||||
// The description of SAML was updated by [RFC1123] and then its actual use was deprecated in [RFC2821]
|
|
||||||
// SAML,
|
|
||||||
|
|
||||||
// Interchange the client and server roles [RFC821]
|
|
||||||
// The actual use of TURN was deprecated in [RFC2821]
|
|
||||||
// TURN,
|
|
||||||
|
|
||||||
// SMTP Responsible Submitter [RFC4405]
|
|
||||||
// Deprecated by [https://datatracker.ietf.org/doc/status-change-change-sender-id-to-historic].
|
|
||||||
// SUBMITTER,
|
|
||||||
|
|
||||||
// Internationalized email address [RFC5336]
|
|
||||||
// Experimental; deprecated in [RFC6531].
|
|
||||||
// UTF8SMTP,
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------
|
|
||||||
/// Verbose [Eric Allman]
|
|
||||||
// VERB,
|
|
||||||
|
|
||||||
/// One message transaction only [Eric Allman]
|
|
||||||
// ONEX,
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// Expand the mailing list [RFC821]
|
|
||||||
/// Command description updated by [RFC5321]
|
|
||||||
Expn,
|
|
||||||
/// Supply helpful information [RFC821]
|
|
||||||
/// Command description updated by [RFC5321]
|
|
||||||
Help,
|
|
||||||
|
|
||||||
/// SMTP and Submit transport of 8bit MIME content [RFC6152]
|
|
||||||
EightBitMime,
|
|
||||||
|
|
||||||
/// Message size declaration [RFC1870]
|
|
||||||
Size(u32),
|
|
||||||
|
|
||||||
/// Chunking [RFC3030]
|
|
||||||
Chunking,
|
|
||||||
|
|
||||||
/// Binary MIME [RFC3030]
|
|
||||||
BinaryMime,
|
|
||||||
|
|
||||||
/// Checkpoint/Restart [RFC1845]
|
|
||||||
Checkpoint,
|
|
||||||
|
|
||||||
/// Deliver By [RFC2852]
|
|
||||||
DeliverBy,
|
|
||||||
|
|
||||||
/// Command Pipelining [RFC2920]
|
|
||||||
Pipelining,
|
|
||||||
|
|
||||||
/// Delivery Status Notification [RFC3461]
|
|
||||||
Dsn,
|
|
||||||
|
|
||||||
/// Extended Turn [RFC1985]
|
|
||||||
/// SMTP [RFC5321] only. Not for use on Submit port 587.
|
|
||||||
Etrn,
|
|
||||||
|
|
||||||
/// Enhanced Status Codes [RFC2034]
|
|
||||||
EnhancedStatusCodes,
|
|
||||||
|
|
||||||
/// Start TLS [RFC3207]
|
|
||||||
StartTls,
|
|
||||||
|
|
||||||
/// Notification of no soliciting [RFC3865]
|
|
||||||
// NoSoliciting,
|
|
||||||
|
|
||||||
/// Message Tracking [RFC3885]
|
|
||||||
Mtrk,
|
|
||||||
|
|
||||||
/// Authenticated TURN [RFC2645]
|
|
||||||
/// SMTP [RFC5321] only. Not for use on Submit port 587.
|
|
||||||
Atrn,
|
|
||||||
|
|
||||||
/// Authentication [RFC4954]
|
|
||||||
Auth(Vec<AuthMechanism>),
|
|
||||||
|
|
||||||
/// Remote Content [RFC4468]
|
|
||||||
/// Submit [RFC6409] only. Not for use with SMTP on port 25.
|
|
||||||
Burl,
|
|
||||||
|
|
||||||
/// Future Message Release [RFC4865]
|
|
||||||
// FutureRelease,
|
|
||||||
|
|
||||||
/// Content Conversion Permission [RFC4141]
|
|
||||||
// ConPerm,
|
|
||||||
|
|
||||||
/// Content Conversion Negotiation [RFC4141]
|
|
||||||
// ConNeg,
|
|
||||||
|
|
||||||
/// Internationalized email address [RFC6531]
|
|
||||||
SmtpUtf8,
|
|
||||||
|
|
||||||
/// Priority Message Handling [RFC6710]
|
|
||||||
// MTPRIORITY,
|
|
||||||
|
|
||||||
/// Require Recipient Valid Since [RFC7293]
|
|
||||||
Rrvs,
|
|
||||||
|
|
||||||
/// Require TLS [RFC8689]
|
|
||||||
RequireTls,
|
|
||||||
|
|
||||||
// Observed ...
|
|
||||||
// TIME,
|
|
||||||
// XACK,
|
|
||||||
// VERP,
|
|
||||||
// VRFY,
|
|
||||||
/// Other
|
|
||||||
Other {
|
|
||||||
keyword: String,
|
|
||||||
params: Vec<String>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Capability {
|
|
||||||
pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
|
|
||||||
match self {
|
|
||||||
Capability::Expn => writer.write_all(b"EXPN"),
|
|
||||||
Capability::Help => writer.write_all(b"HELP"),
|
|
||||||
Capability::EightBitMime => writer.write_all(b"8BITMIME"),
|
|
||||||
Capability::Size(number) => writer.write_all(format!("SIZE {}", number).as_bytes()),
|
|
||||||
Capability::Chunking => writer.write_all(b"CHUNKING"),
|
|
||||||
Capability::BinaryMime => writer.write_all(b"BINARYMIME"),
|
|
||||||
Capability::Checkpoint => writer.write_all(b"CHECKPOINT"),
|
|
||||||
Capability::DeliverBy => writer.write_all(b"DELIVERBY"),
|
|
||||||
Capability::Pipelining => writer.write_all(b"PIPELINING"),
|
|
||||||
Capability::Dsn => writer.write_all(b"DSN"),
|
|
||||||
Capability::Etrn => writer.write_all(b"ETRN"),
|
|
||||||
Capability::EnhancedStatusCodes => writer.write_all(b"ENHANCEDSTATUSCODES"),
|
|
||||||
Capability::StartTls => writer.write_all(b"STARTTLS"),
|
|
||||||
Capability::Mtrk => writer.write_all(b"MTRK"),
|
|
||||||
Capability::Atrn => writer.write_all(b"ATRN"),
|
|
||||||
Capability::Auth(mechanisms) => {
|
|
||||||
if let Some((tail, head)) = mechanisms.split_last() {
|
|
||||||
writer.write_all(b"AUTH ")?;
|
|
||||||
|
|
||||||
for mechanism in head {
|
|
||||||
mechanism.serialize(writer)?;
|
|
||||||
writer.write_all(b" ")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
tail.serialize(writer)
|
|
||||||
} else {
|
|
||||||
writer.write_all(b"AUTH")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Capability::Burl => writer.write_all(b"BURL"),
|
|
||||||
Capability::SmtpUtf8 => writer.write_all(b"SMTPUTF8"),
|
|
||||||
Capability::Rrvs => writer.write_all(b"RRVS"),
|
|
||||||
Capability::RequireTls => writer.write_all(b"REQUIRETLS"),
|
|
||||||
Capability::Other { keyword, params } => {
|
|
||||||
if let Some((tail, head)) = params.split_last() {
|
|
||||||
writer.write_all(keyword.as_bytes())?;
|
|
||||||
writer.write_all(b" ")?;
|
|
||||||
|
|
||||||
for param in head {
|
|
||||||
writer.write_all(param.as_bytes())?;
|
|
||||||
writer.write_all(b" ")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.write_all(tail.as_bytes())
|
|
||||||
} else {
|
|
||||||
writer.write_all(keyword.as_bytes())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
|
|
||||||
pub enum ReplyCode {
|
|
||||||
/// 211 System status, or system help reply
|
|
||||||
SystemStatus,
|
|
||||||
/// 214 Help message
|
|
||||||
///
|
|
||||||
/// Information on how to use the receiver or the meaning of a particular non-standard
|
|
||||||
/// command; this reply is useful only to the human user.
|
|
||||||
HelpMessage,
|
|
||||||
/// 220 <domain> Service ready
|
|
||||||
Ready,
|
|
||||||
/// 221 <domain> Service closing transmission channel
|
|
||||||
ClosingChannel,
|
|
||||||
/// 250 Requested mail action okay, completed
|
|
||||||
Ok,
|
|
||||||
/// 251 User not local; will forward to <forward-path>
|
|
||||||
UserNotLocalWillForward,
|
|
||||||
/// 252 Cannot VRFY user, but will accept message and attempt delivery
|
|
||||||
CannotVrfy,
|
|
||||||
/// 354 Start mail input; end with <CRLF>.<CRLF>
|
|
||||||
StartMailInput,
|
|
||||||
/// 421 <domain> Service not available, closing transmission channel
|
|
||||||
///
|
|
||||||
/// This may be a reply to any command if the service knows it must shut down.
|
|
||||||
NotAvailable,
|
|
||||||
/// 450 Requested mail action not taken: mailbox unavailable
|
|
||||||
///
|
|
||||||
/// E.g., mailbox busy or temporarily blocked for policy reasons.
|
|
||||||
MailboxTemporarilyUnavailable,
|
|
||||||
/// 451 Requested action aborted: local error in processing
|
|
||||||
ProcessingError,
|
|
||||||
/// 452 Requested action not taken: insufficient system storage
|
|
||||||
InsufficientStorage,
|
|
||||||
/// 455 Server unable to accommodate parameters
|
|
||||||
UnableToAccommodateParameters,
|
|
||||||
/// 500 Syntax error, command unrecognized
|
|
||||||
SyntaxError,
|
|
||||||
/// 501 Syntax error in parameters or arguments
|
|
||||||
ParameterSyntaxError,
|
|
||||||
/// 502 Command not implemented
|
|
||||||
CommandNotImplemented,
|
|
||||||
/// 503 Bad sequence of commands
|
|
||||||
BadSequence,
|
|
||||||
/// 504 Command parameter not implemented
|
|
||||||
ParameterNotImplemented,
|
|
||||||
/// 521 <domain> does not accept mail (see RFC 1846)
|
|
||||||
NoMailService,
|
|
||||||
/// 550 Requested action not taken: mailbox unavailable
|
|
||||||
///
|
|
||||||
/// E.g. mailbox not found, no access, or command rejected for policy reasons.
|
|
||||||
MailboxPermanentlyUnavailable,
|
|
||||||
/// 551 User not local; please try <forward-path>
|
|
||||||
UserNotLocal,
|
|
||||||
/// 552 Requested mail action aborted: exceeded storage allocation
|
|
||||||
ExceededStorageAllocation,
|
|
||||||
/// 553 Requested action not taken: mailbox name not allowed
|
|
||||||
///
|
|
||||||
/// E.g. mailbox syntax incorrect.
|
|
||||||
MailboxNameNotAllowed,
|
|
||||||
/// 554 Transaction failed
|
|
||||||
///
|
|
||||||
/// Or, in the case of a connection-opening response, "No SMTP service here".
|
|
||||||
TransactionFailed,
|
|
||||||
/// 555 MAIL FROM/RCPT TO parameters not recognized or not implemented
|
|
||||||
ParametersNotImplemented,
|
|
||||||
/// Miscellaneous reply codes
|
|
||||||
Other(u16),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ReplyCode {
|
|
||||||
pub fn is_completed(&self) -> bool {
|
|
||||||
let code = u16::from(*self);
|
|
||||||
code > 199 && code < 300
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_accepted(&self) -> bool {
|
|
||||||
let code = u16::from(*self);
|
|
||||||
code > 299 && code < 400
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_temporary_error(&self) -> bool {
|
|
||||||
let code = u16::from(*self);
|
|
||||||
code > 399 && code < 500
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_permanent_error(&self) -> bool {
|
|
||||||
let code = u16::from(*self);
|
|
||||||
code > 499 && code < 600
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<u16> for ReplyCode {
|
|
||||||
fn from(value: u16) -> Self {
|
|
||||||
match value {
|
|
||||||
211 => ReplyCode::SystemStatus,
|
|
||||||
214 => ReplyCode::HelpMessage,
|
|
||||||
220 => ReplyCode::Ready,
|
|
||||||
221 => ReplyCode::ClosingChannel,
|
|
||||||
250 => ReplyCode::Ok,
|
|
||||||
251 => ReplyCode::UserNotLocalWillForward,
|
|
||||||
252 => ReplyCode::CannotVrfy,
|
|
||||||
354 => ReplyCode::StartMailInput,
|
|
||||||
421 => ReplyCode::NotAvailable,
|
|
||||||
450 => ReplyCode::MailboxTemporarilyUnavailable,
|
|
||||||
451 => ReplyCode::ProcessingError,
|
|
||||||
452 => ReplyCode::InsufficientStorage,
|
|
||||||
455 => ReplyCode::UnableToAccommodateParameters,
|
|
||||||
500 => ReplyCode::SyntaxError,
|
|
||||||
501 => ReplyCode::ParameterSyntaxError,
|
|
||||||
502 => ReplyCode::CommandNotImplemented,
|
|
||||||
503 => ReplyCode::BadSequence,
|
|
||||||
504 => ReplyCode::ParameterNotImplemented,
|
|
||||||
521 => ReplyCode::NoMailService,
|
|
||||||
550 => ReplyCode::MailboxPermanentlyUnavailable,
|
|
||||||
551 => ReplyCode::UserNotLocal,
|
|
||||||
552 => ReplyCode::ExceededStorageAllocation,
|
|
||||||
553 => ReplyCode::MailboxNameNotAllowed,
|
|
||||||
554 => ReplyCode::TransactionFailed,
|
|
||||||
555 => ReplyCode::ParametersNotImplemented,
|
|
||||||
_ => ReplyCode::Other(value),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ReplyCode> for u16 {
|
|
||||||
fn from(value: ReplyCode) -> Self {
|
|
||||||
match value {
|
|
||||||
ReplyCode::SystemStatus => 211,
|
|
||||||
ReplyCode::HelpMessage => 214,
|
|
||||||
ReplyCode::Ready => 220,
|
|
||||||
ReplyCode::ClosingChannel => 221,
|
|
||||||
ReplyCode::Ok => 250,
|
|
||||||
ReplyCode::UserNotLocalWillForward => 251,
|
|
||||||
ReplyCode::CannotVrfy => 252,
|
|
||||||
ReplyCode::StartMailInput => 354,
|
|
||||||
ReplyCode::NotAvailable => 421,
|
|
||||||
ReplyCode::MailboxTemporarilyUnavailable => 450,
|
|
||||||
ReplyCode::ProcessingError => 451,
|
|
||||||
ReplyCode::InsufficientStorage => 452,
|
|
||||||
ReplyCode::UnableToAccommodateParameters => 455,
|
|
||||||
ReplyCode::SyntaxError => 500,
|
|
||||||
ReplyCode::ParameterSyntaxError => 501,
|
|
||||||
ReplyCode::CommandNotImplemented => 502,
|
|
||||||
ReplyCode::BadSequence => 503,
|
|
||||||
ReplyCode::ParameterNotImplemented => 504,
|
|
||||||
ReplyCode::NoMailService => 521,
|
|
||||||
ReplyCode::MailboxPermanentlyUnavailable => 550,
|
|
||||||
ReplyCode::UserNotLocal => 551,
|
|
||||||
ReplyCode::ExceededStorageAllocation => 552,
|
|
||||||
ReplyCode::MailboxNameNotAllowed => 553,
|
|
||||||
ReplyCode::TransactionFailed => 554,
|
|
||||||
ReplyCode::ParametersNotImplemented => 555,
|
|
||||||
ReplyCode::Other(v) => v,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum AuthMechanism {
|
|
||||||
Plain,
|
|
||||||
Login,
|
|
||||||
GssApi,
|
|
||||||
|
|
||||||
CramMd5,
|
|
||||||
CramSha1,
|
|
||||||
ScramMd5,
|
|
||||||
DigestMd5,
|
|
||||||
Ntlm,
|
|
||||||
|
|
||||||
Other(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AuthMechanism {
|
|
||||||
pub fn serialize(&self, writer: &mut impl Write) -> std::io::Result<()> {
|
|
||||||
match self {
|
|
||||||
AuthMechanism::Plain => writer.write_all(b"PLAIN"),
|
|
||||||
AuthMechanism::Login => writer.write_all(b"LOGIN"),
|
|
||||||
AuthMechanism::GssApi => writer.write_all(b"GSSAPI"),
|
|
||||||
|
|
||||||
AuthMechanism::CramMd5 => writer.write_all(b"CRAM-MD5"),
|
|
||||||
AuthMechanism::CramSha1 => writer.write_all(b"CRAM-SHA1"),
|
|
||||||
AuthMechanism::ScramMd5 => writer.write_all(b"SCRAM-MD5"),
|
|
||||||
AuthMechanism::DigestMd5 => writer.write_all(b"DIGEST-MD5"),
|
|
||||||
AuthMechanism::Ntlm => writer.write_all(b"NTLM"),
|
|
||||||
|
|
||||||
AuthMechanism::Other(other) => writer.write_all(other.as_bytes()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A string containing of tab, space and printable ASCII characters
|
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
pub struct TextString<'a>(pub(crate) Cow<'a, str>);
|
|
||||||
|
|
||||||
impl<'a> TextString<'a> {
|
|
||||||
pub fn new(s: &'a str) -> Result<Self, InvalidTextString> {
|
|
||||||
match s.as_bytes().iter().all(|&b| is_text_string_byte(b)) {
|
|
||||||
true => Ok(TextString(Cow::Borrowed(s))),
|
|
||||||
false => Err(InvalidTextString(())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn into_owned(self) -> TextString<'static> {
|
|
||||||
TextString(self.0.into_owned().into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for TextString<'_> {
|
|
||||||
type Target = str;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for TextString<'_> {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct InvalidTextString(());
|
|
||||||
|
|
||||||
impl fmt::Display for InvalidTextString {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "input contains invalid characters")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for InvalidTextString {}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::{Capability, ReplyCode, Response, TextString};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialize_greeting() {
|
|
||||||
let tests = &[
|
|
||||||
(
|
|
||||||
Response::Greeting {
|
|
||||||
domain: "example.org".into(),
|
|
||||||
text: "".into(),
|
|
||||||
},
|
|
||||||
b"220 example.org\r\n".as_ref(),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
Response::Greeting {
|
|
||||||
domain: "example.org".into(),
|
|
||||||
text: "A".into(),
|
|
||||||
},
|
|
||||||
b"220 example.org A\r\n".as_ref(),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
Response::Greeting {
|
|
||||||
domain: "example.org".into(),
|
|
||||||
text: "A\nB".into(),
|
|
||||||
},
|
|
||||||
b"220-example.org A\r\n220 B\r\n".as_ref(),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
Response::Greeting {
|
|
||||||
domain: "example.org".into(),
|
|
||||||
text: "A\nB\nC".into(),
|
|
||||||
},
|
|
||||||
b"220-example.org A\r\n220-B\r\n220 C\r\n".as_ref(),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (test, expected) in tests.iter() {
|
|
||||||
let mut got = Vec::new();
|
|
||||||
test.serialize(&mut got).unwrap();
|
|
||||||
assert_eq!(expected, &got);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialize_ehlo() {
|
|
||||||
let tests = &[
|
|
||||||
(
|
|
||||||
Response::Ehlo {
|
|
||||||
domain: "example.org".into(),
|
|
||||||
greet: None,
|
|
||||||
capabilities: vec![],
|
|
||||||
},
|
|
||||||
b"250 example.org\r\n".as_ref(),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
Response::Ehlo {
|
|
||||||
domain: "example.org".into(),
|
|
||||||
greet: Some("...".into()),
|
|
||||||
capabilities: vec![],
|
|
||||||
},
|
|
||||||
b"250 example.org ...\r\n".as_ref(),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
Response::Ehlo {
|
|
||||||
domain: "example.org".into(),
|
|
||||||
greet: Some("...".into()),
|
|
||||||
capabilities: vec![Capability::StartTls],
|
|
||||||
},
|
|
||||||
b"250-example.org ...\r\n250 STARTTLS\r\n".as_ref(),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
Response::Ehlo {
|
|
||||||
domain: "example.org".into(),
|
|
||||||
greet: Some("...".into()),
|
|
||||||
capabilities: vec![Capability::StartTls, Capability::Size(12345)],
|
|
||||||
},
|
|
||||||
b"250-example.org ...\r\n250-STARTTLS\r\n250 SIZE 12345\r\n".as_ref(),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (test, expected) in tests.iter() {
|
|
||||||
let mut got = Vec::new();
|
|
||||||
test.serialize(&mut got).unwrap();
|
|
||||||
assert_eq!(expected, &got);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_serialize_other() {
|
|
||||||
let tests = &[
|
|
||||||
(
|
|
||||||
Response::Other {
|
|
||||||
code: ReplyCode::StartMailInput,
|
|
||||||
lines: vec![],
|
|
||||||
},
|
|
||||||
b"354\r\n".as_ref(),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
Response::Other {
|
|
||||||
code: ReplyCode::StartMailInput,
|
|
||||||
lines: vec![TextString::new("A").unwrap()],
|
|
||||||
},
|
|
||||||
b"354 A\r\n".as_ref(),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
Response::Other {
|
|
||||||
code: ReplyCode::StartMailInput,
|
|
||||||
lines: vec![TextString::new("A").unwrap(), TextString::new("B").unwrap()],
|
|
||||||
},
|
|
||||||
b"354-A\r\n354 B\r\n".as_ref(),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (test, expected) in tests.iter() {
|
|
||||||
let mut got = Vec::new();
|
|
||||||
test.serialize(&mut got).unwrap();
|
|
||||||
assert_eq!(expected, &got);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
use instant_smtp::types::{Command, Response};
|
use instant_smtp::{Command, Response};
|
||||||
use nom::FindSubstring;
|
use nom::FindSubstring;
|
||||||
|
|
||||||
fn parse_trace(mut trace: &[u8]) {
|
fn parse_trace(mut trace: &[u8]) {
|
||||||
|
|
Loading…
Reference in New Issue