#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
#[serde(untagged)]
pub enum ReplyCode {
Code {
code: u16,
},
Enhanced {
code: u16,
enhanced: String,
},
}
impl From<lettre::transport::smtp::response::Code> for ReplyCode {
#[inline]
fn from(value: lettre::transport::smtp::response::Code) -> Self {
#[allow(clippy::expect_used)]
Self::Code {
code: value.to_string().parse().expect("code format is valid"),
}
}
}
const ENHANCED: i32 = 0;
const SIMPLE: i32 = 1;
impl ReplyCode {
#[must_use]
#[inline]
pub fn is_error(&self) -> bool {
match self {
Self::Code { code, .. } | Self::Enhanced { code, .. } => code / 100 >= 4,
}
}
#[must_use]
#[inline]
pub fn value(&self) -> u16 {
match self {
Self::Code { code, .. } | Self::Enhanced { code, .. } => *code,
}
}
#[must_use]
#[inline]
pub fn details(&self) -> Option<&str> {
match self {
Self::Enhanced { enhanced, .. } => Some(enhanced),
Self::Code { .. } => None,
}
}
fn try_parse(which: i32, words: &[&str]) -> Option<Self> {
match (which, words) {
(ENHANCED, [_, "", ..]) => None,
(ENHANCED, [code, enhanced, ..]) => {
let mut enhanced = enhanced.splitn(3, '.').map(str::parse::<u16>);
let (a, b, c) = (
enhanced.next()?.ok()?,
enhanced.next()?.ok()?,
enhanced.next()?.ok()?,
);
#[allow(clippy::unreachable)]
Some(Self::Enhanced {
code: match Self::try_parse(SIMPLE, &[code])? {
Self::Code { code, .. } => code,
Self::Enhanced { .. } => unreachable!(),
},
enhanced: format!("{a}.{b}.{c}"),
})
}
(SIMPLE, [code, ..]) => Some(Self::Code {
code: code.parse::<u16>().ok()?,
}),
_ => None,
}
}
pub(super) fn from_str(s: &str) -> anyhow::Result<(Self, String)> {
for i in ENHANCED..=SIMPLE {
let words = s.split([' ', '-']).collect::<Vec<&str>>();
if let Some(code) = Self::try_parse(i, words.as_slice()) {
let code_len = code.to_string().len();
#[allow(clippy::string_slice, clippy::indexing_slicing)]
return Ok((code, s[code_len..].to_string()));
}
}
anyhow::bail!("cannot parse {s:?}")
}
}
impl std::fmt::Display for ReplyCode {
#[inline]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Code { code } => f.write_fmt(format_args!("{code}")),
Self::Enhanced { code, enhanced } => f.write_fmt(format_args!("{code} {enhanced}")),
}
}
}
#[cfg(test)]
mod tests {
use crate::ReplyCode;
#[rstest::rstest]
#[case(
"250",
(&ReplyCode::Code { code: 250 }, ""),
"250"
)]
#[case(
"504 5.5.4",
(&ReplyCode::Enhanced {
code: 504,
enhanced: "5.5.4".to_owned(),
},
""),
"504 5.5.4",
)]
#[case(
"250-2.0.0",
(&ReplyCode::Enhanced {
code: 250,
enhanced: "2.0.0".to_owned(),
},
""),
"250 2.0.0",
)]
#[case(
"250 ",
(&ReplyCode::Code { code: 250 }, " "),
"250"
)]
#[case(
"504 5.5.4 ",
(&ReplyCode::Enhanced {
code: 504,
enhanced: "5.5.4".to_owned(),
},
" "),
"504 5.5.4",
)]
#[case(
"250-2.0.0 ",
(&ReplyCode::Enhanced {
code: 250,
enhanced: "2.0.0".to_owned(),
},
" "),
"250 2.0.0",
)]
fn parse_reply(
#[case] input: &str,
#[case] expected: (&ReplyCode, &str),
#[case] to_string: &str,
) {
let (code, message) = ReplyCode::from_str(input).unwrap();
pretty_assertions::assert_eq!(code, *expected.0);
pretty_assertions::assert_eq!(code.to_string(), to_string);
pretty_assertions::assert_eq!(message, expected.1);
}
}