use core::fmt;
use std::error::Error as StdError;
use crate::protocol::EnhancedStatus;
#[derive(Debug)]
pub enum SmtpError {
Io(IoError),
Protocol(ProtocolError),
Auth(AuthError),
InvalidInput(InvalidInputError),
}
impl fmt::Display for SmtpError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(e) => write!(f, "smtp transport error: {e}"),
Self::Protocol(e) => write!(f, "smtp protocol error: {e}"),
Self::Auth(e) => write!(f, "smtp auth error: {e}"),
Self::InvalidInput(e) => write!(f, "smtp invalid input: {e}"),
}
}
}
impl StdError for SmtpError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match self {
Self::Io(e) => Some(e),
Self::Protocol(e) => Some(e),
Self::Auth(e) => Some(e),
Self::InvalidInput(e) => Some(e),
}
}
}
impl From<IoError> for SmtpError {
fn from(value: IoError) -> Self {
Self::Io(value)
}
}
impl From<ProtocolError> for SmtpError {
fn from(value: ProtocolError) -> Self {
Self::Protocol(value)
}
}
impl From<AuthError> for SmtpError {
fn from(value: AuthError) -> Self {
Self::Auth(value)
}
}
impl From<InvalidInputError> for SmtpError {
fn from(value: InvalidInputError) -> Self {
Self::InvalidInput(value)
}
}
#[derive(Debug)]
pub struct IoError {
message: String,
source: Option<Box<dyn StdError + Send + Sync + 'static>>,
}
impl IoError {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
source: None,
}
}
pub fn with_source<E>(message: impl Into<String>, source: E) -> Self
where
E: StdError + Send + Sync + 'static,
{
Self {
message: message.into(),
source: Some(Box::new(source)),
}
}
pub fn message(&self) -> &str {
&self.message
}
}
impl fmt::Display for IoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl StdError for IoError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
self.source
.as_deref()
.map(|e| e as &(dyn StdError + 'static))
}
}
impl From<std::io::Error> for IoError {
fn from(e: std::io::Error) -> Self {
let message = e.to_string();
Self::with_source(message, e)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SmtpOp {
Greeting,
Ehlo,
StartTls,
AuthPlain,
AuthLogin,
AuthXOAuth2,
AuthScramSha256,
MailFrom,
RcptTo,
Data,
Quit,
}
impl SmtpOp {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Greeting => "greeting",
Self::Ehlo => "EHLO",
Self::StartTls => "STARTTLS",
Self::AuthPlain => "AUTH PLAIN",
Self::AuthLogin => "AUTH LOGIN",
Self::AuthXOAuth2 => "AUTH XOAUTH2",
Self::AuthScramSha256 => "AUTH SCRAM-SHA-256",
Self::MailFrom => "MAIL FROM",
Self::RcptTo => "RCPT TO",
Self::Data => "DATA",
Self::Quit => "QUIT",
}
}
}
impl fmt::Display for SmtpOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum ProtocolError {
UnexpectedCode {
during: SmtpOp,
expected_class: u8,
actual: u16,
enhanced: Option<EnhancedStatus>,
message: String,
},
Malformed(String),
UnexpectedClose,
LineTooLong,
InconsistentMultiline {
first: u16,
later: u16,
},
ExtensionUnavailable {
name: &'static str,
},
StartTlsBufferResidue {
byte_count: usize,
},
}
impl fmt::Display for ProtocolError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnexpectedCode {
during,
expected_class,
actual,
enhanced,
message,
} => {
if let Some(es) = enhanced {
write!(
f,
"during {during}, expected {expected_class}xx response but received {actual} [{es}]: {message}",
)
} else {
write!(
f,
"during {during}, expected {expected_class}xx response but received {actual}: {message}",
)
}
}
Self::Malformed(s) => write!(f, "malformed server reply: {s}"),
Self::UnexpectedClose => f.write_str("server closed connection unexpectedly"),
Self::LineTooLong => f.write_str("server reply line exceeded SMTP line-length limit"),
Self::InconsistentMultiline { first, later } => {
write!(f, "multi-line reply mixed codes {first} and {later}",)
}
Self::ExtensionUnavailable { name } => {
write!(f, "server did not advertise the {name} extension")
}
Self::StartTlsBufferResidue { byte_count } => {
write!(
f,
"{byte_count} unread byte(s) in receive buffer after STARTTLS reply \
(possible command-injection attack — aborting upgrade)"
)
}
}
}
}
impl StdError for ProtocolError {}
#[derive(Debug)]
#[non_exhaustive]
pub enum AuthError {
Rejected {
code: u16,
enhanced: Option<EnhancedStatus>,
message: String,
},
UnsupportedMechanism,
MalformedChallenge(String),
Other(&'static str),
}
impl fmt::Display for AuthError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Rejected {
code,
enhanced,
message,
} => {
if let Some(es) = enhanced {
write!(
f,
"server rejected authentication ({code} [{es}]): {message}"
)
} else {
write!(f, "server rejected authentication ({code}): {message}")
}
}
Self::UnsupportedMechanism => {
#[cfg(feature = "xoauth2")]
{
f.write_str(
"server did not advertise an AUTH mechanism supported by this client \
(this client knows PLAIN, LOGIN, and XOAUTH2)",
)
}
#[cfg(not(feature = "xoauth2"))]
{
f.write_str(
"server did not advertise an AUTH mechanism supported by this client \
(this client knows PLAIN and LOGIN; XOAUTH2 was not compiled in)",
)
}
}
Self::MalformedChallenge(s) => {
write!(f, "server sent a malformed AUTH challenge: {s}")
}
Self::Other(detail) => {
write!(f, "authentication failed: {detail}")
}
}
}
}
impl StdError for AuthError {}
#[derive(Debug)]
pub struct InvalidInputError {
reason: &'static str,
}
impl InvalidInputError {
pub const fn new(reason: &'static str) -> Self {
Self { reason }
}
pub const fn reason(&self) -> &'static str {
self.reason
}
}
impl fmt::Display for InvalidInputError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.reason)
}
}
impl StdError for InvalidInputError {}