#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::fmt;
use std::error::Error;
use use_email_address::AddressValidationError;
use use_email_envelope::{MailFromPath, RcptToPath};
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SmtpError {
Address(AddressValidationError),
InvalidReplyCode,
Empty,
InvalidEnhancedStatus,
}
impl fmt::Display for SmtpError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Address(error) => write!(formatter, "{error}"),
Self::InvalidReplyCode => {
formatter.write_str("SMTP reply code must be between 100 and 599")
}
Self::Empty => formatter.write_str("SMTP value cannot be empty"),
Self::InvalidEnhancedStatus => formatter.write_str("invalid SMTP enhanced status code"),
}
}
}
impl Error for SmtpError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Address(error) => Some(error),
Self::InvalidReplyCode | Self::Empty | Self::InvalidEnhancedStatus => None,
}
}
}
impl From<AddressValidationError> for SmtpError {
fn from(value: AddressValidationError) -> Self {
Self::Address(value)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SmtpCommand {
Helo(String),
Ehlo(String),
MailFrom(MailFrom),
RcptTo(RcptTo),
Data(DataCommand),
Quit(QuitCommand),
StartTls,
}
impl fmt::Display for SmtpCommand {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Helo(domain) => write!(formatter, "HELO {domain}"),
Self::Ehlo(domain) => write!(formatter, "EHLO {domain}"),
Self::MailFrom(command) => write!(formatter, "{command}"),
Self::RcptTo(command) => write!(formatter, "{command}"),
Self::Data(command) => write!(formatter, "{command}"),
Self::Quit(command) => write!(formatter, "{command}"),
Self::StartTls => formatter.write_str("STARTTLS"),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct SmtpReplyCode(u16);
impl SmtpReplyCode {
pub const fn new(value: u16) -> Result<Self, SmtpError> {
if value >= 100 && value <= 599 {
Ok(Self(value))
} else {
Err(SmtpError::InvalidReplyCode)
}
}
#[must_use]
pub const fn value(self) -> u16 {
self.0
}
}
impl fmt::Display for SmtpReplyCode {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}", self.0)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct SmtpEnhancedStatusCode {
class: u8,
subject: u16,
detail: u16,
}
impl SmtpEnhancedStatusCode {
pub const fn new(class: u8, subject: u16, detail: u16) -> Result<Self, SmtpError> {
if matches!(class, 2 | 4 | 5) {
Ok(Self {
class,
subject,
detail,
})
} else {
Err(SmtpError::InvalidEnhancedStatus)
}
}
}
impl fmt::Display for SmtpEnhancedStatusCode {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}.{}.{}", self.class, self.subject, self.detail)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SmtpReply {
code: SmtpReplyCode,
enhanced_status: Option<SmtpEnhancedStatusCode>,
text: String,
}
impl SmtpReply {
pub fn new(code: SmtpReplyCode, text: impl AsRef<str>) -> Result<Self, SmtpError> {
Self::with_enhanced_status(code, None, text)
}
pub fn with_enhanced_status(
code: SmtpReplyCode,
enhanced_status: Option<SmtpEnhancedStatusCode>,
text: impl AsRef<str>,
) -> Result<Self, SmtpError> {
let text = validate_text(text.as_ref())?;
Ok(Self {
code,
enhanced_status,
text: text.to_owned(),
})
}
#[must_use]
pub const fn code(&self) -> SmtpReplyCode {
self.code
}
}
impl fmt::Display for SmtpReply {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(status) = self.enhanced_status {
write!(formatter, "{} {} {}", self.code, status, self.text)
} else {
write!(formatter, "{} {}", self.code, self.text)
}
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum SmtpExtension {
StartTls,
EightBitMime,
SmtpUtf8,
Size(Option<u64>),
Auth(String),
Other(String),
}
impl fmt::Display for SmtpExtension {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::StartTls => formatter.write_str("STARTTLS"),
Self::EightBitMime => formatter.write_str("8BITMIME"),
Self::SmtpUtf8 => formatter.write_str("SMTPUTF8"),
Self::Size(Some(limit)) => write!(formatter, "SIZE {limit}"),
Self::Size(None) => formatter.write_str("SIZE"),
Self::Auth(mechanisms) => write!(formatter, "AUTH {mechanisms}"),
Self::Other(value) => formatter.write_str(value),
}
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct EhloKeyword(String);
impl EhloKeyword {
pub fn new(value: impl AsRef<str>) -> Result<Self, SmtpError> {
validate_text(value.as_ref()).map(|value| Self(value.to_ascii_uppercase()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for EhloKeyword {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct MailFrom(MailFromPath);
impl MailFrom {
pub fn new(value: impl AsRef<str>) -> Result<Self, SmtpError> {
Ok(Self(MailFromPath::new(value)?))
}
#[must_use]
pub const fn null() -> Self {
Self(MailFromPath::null())
}
#[must_use]
pub const fn path(&self) -> &MailFromPath {
&self.0
}
}
impl fmt::Display for MailFrom {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "MAIL FROM:{}", self.0)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct RcptTo(RcptToPath);
impl RcptTo {
pub fn new(value: impl AsRef<str>) -> Result<Self, SmtpError> {
Ok(Self(RcptToPath::new(value)?))
}
#[must_use]
pub const fn path(&self) -> &RcptToPath {
&self.0
}
}
impl fmt::Display for RcptTo {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "RCPT TO:{}", self.0)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct DataCommand;
impl fmt::Display for DataCommand {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("DATA")
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct QuitCommand;
impl fmt::Display for QuitCommand {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("QUIT")
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct StartTlsCapability;
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct EightBitMimeCapability;
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct SmtpUtf8Capability;
fn validate_text(value: &str) -> Result<&str, SmtpError> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(SmtpError::Empty);
}
if trimmed
.chars()
.any(|character| matches!(character, '\r' | '\n'))
{
return Err(SmtpError::Empty);
}
Ok(trimmed)
}
#[cfg(test)]
mod tests {
use super::{MailFrom, RcptTo, SmtpCommand, SmtpEnhancedStatusCode, SmtpReply, SmtpReplyCode};
#[test]
fn renders_commands_and_replies() -> Result<(), Box<dyn std::error::Error>> {
let mail_from = SmtpCommand::MailFrom(MailFrom::new("bounce@example.com")?);
let rcpt_to = SmtpCommand::RcptTo(RcptTo::new("jane@example.com")?);
let reply = SmtpReply::new(SmtpReplyCode::new(250)?, "OK")?;
let enhanced = SmtpReply::with_enhanced_status(
SmtpReplyCode::new(250)?,
Some(SmtpEnhancedStatusCode::new(2, 0, 0)?),
"OK",
)?;
assert_eq!(mail_from.to_string(), "MAIL FROM:<bounce@example.com>");
assert_eq!(rcpt_to.to_string(), "RCPT TO:<jane@example.com>");
assert_eq!(reply.to_string(), "250 OK");
assert_eq!(enhanced.to_string(), "250 2.0.0 OK");
Ok(())
}
}