use std::{
fmt::{Display, Formatter, Result as FmtResult, Write},
mem,
slice::Iter,
str::FromStr,
};
use email_encoding::headers::writer::EmailWriter;
use super::parsers;
use crate::address::{Address, AddressError};
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct Mailbox {
pub name: Option<String>,
pub email: Address,
}
impl Mailbox {
pub fn new(name: Option<String>, email: Address) -> Self {
Mailbox { name, email }
}
pub(crate) fn encode(&self, w: &mut EmailWriter<'_>) -> FmtResult {
if let Some(name) = &self.name {
email_encoding::headers::quoted_string::encode(name, w)?;
w.space();
w.write_char('<')?;
}
w.write_str(self.email.as_ref())?;
if self.name.is_some() {
w.write_char('>')?;
}
Ok(())
}
}
impl Display for Mailbox {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
if let Some(name) = &self.name {
let name = name.trim();
if !name.is_empty() {
write_word(f, name)?;
f.write_str(" <")?;
self.email.fmt(f)?;
return f.write_char('>');
}
}
self.email.fmt(f)
}
}
impl<S: Into<String>, T: Into<String>> TryFrom<(S, T)> for Mailbox {
type Error = AddressError;
fn try_from(header: (S, T)) -> Result<Self, Self::Error> {
let (name, address) = header;
Ok(Mailbox::new(Some(name.into()), address.into().parse()?))
}
}
impl FromStr for Mailbox {
type Err = AddressError;
fn from_str(src: &str) -> Result<Mailbox, Self::Err> {
let (_rest, (name, (user, domain))) = parsers::mailbox(src).map_err(|_errs| {
AddressError::InvalidInput
})?;
let mailbox = Mailbox::new(name, Address::new(user, domain)?);
Ok(mailbox)
}
}
impl From<Address> for Mailbox {
fn from(value: Address) -> Self {
Self::new(None, value)
}
}
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
pub struct Mailboxes(Vec<Mailbox>);
impl Mailboxes {
pub fn new() -> Self {
Mailboxes(Vec::new())
}
pub fn with(mut self, mbox: Mailbox) -> Self {
self.0.push(mbox);
self
}
pub fn push(&mut self, mbox: Mailbox) {
self.0.push(mbox);
}
pub fn into_single(self) -> Option<Mailbox> {
self.into()
}
pub fn iter(&self) -> Iter<'_, Mailbox> {
self.0.iter()
}
pub(crate) fn encode(&self, w: &mut EmailWriter<'_>) -> FmtResult {
let mut first = true;
for mailbox in self.iter() {
if !mem::take(&mut first) {
w.write_char(',')?;
w.space();
}
mailbox.encode(w)?;
}
Ok(())
}
}
impl Default for Mailboxes {
fn default() -> Self {
Self::new()
}
}
impl From<Mailbox> for Mailboxes {
fn from(mailbox: Mailbox) -> Self {
Mailboxes(vec![mailbox])
}
}
impl From<Mailboxes> for Option<Mailbox> {
fn from(mailboxes: Mailboxes) -> Option<Mailbox> {
mailboxes.into_iter().next()
}
}
impl From<Vec<Mailbox>> for Mailboxes {
fn from(vec: Vec<Mailbox>) -> Self {
Mailboxes(vec)
}
}
impl From<Mailboxes> for Vec<Mailbox> {
fn from(mailboxes: Mailboxes) -> Vec<Mailbox> {
mailboxes.0
}
}
impl FromIterator<Mailbox> for Mailboxes {
fn from_iter<T: IntoIterator<Item = Mailbox>>(iter: T) -> Self {
Self(Vec::from_iter(iter))
}
}
impl Extend<Mailbox> for Mailboxes {
fn extend<T: IntoIterator<Item = Mailbox>>(&mut self, iter: T) {
self.0.extend(iter);
}
}
impl IntoIterator for Mailboxes {
type Item = Mailbox;
type IntoIter = ::std::vec::IntoIter<Mailbox>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl Display for Mailboxes {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let mut iter = self.iter();
if let Some(mbox) = iter.next() {
mbox.fmt(f)?;
for mbox in iter {
f.write_str(", ")?;
mbox.fmt(f)?;
}
}
Ok(())
}
}
impl FromStr for Mailboxes {
type Err = AddressError;
fn from_str(src: &str) -> Result<Self, Self::Err> {
let mut mailboxes = Vec::new();
let (_rest, parsed_mailboxes) = parsers::mailbox_list(src).map_err(|_errs| {
AddressError::InvalidInput
})?;
for (name, (user, domain)) in parsed_mailboxes {
mailboxes.push(Mailbox::new(name, Address::new(user, domain)?));
}
Ok(Mailboxes(mailboxes))
}
}
fn write_word(f: &mut Formatter<'_>, s: &str) -> FmtResult {
if s.as_bytes().iter().copied().all(is_valid_atom_char) {
f.write_str(s)
} else {
f.write_char('"')?;
for c in s.chars() {
write_quoted_string_char(f, c)?;
}
f.write_char('"')?;
Ok(())
}
}
fn is_valid_atom_char(c: u8) -> bool {
matches!(c,
b'\t' |
b' ' |
b'!' |
b'#' |
b'$' |
b'%' |
b'&' |
b'\'' |
b'*' |
b'+' |
b'-' |
b'/' |
b'0'..=b'8' |
b'=' |
b'?' |
b'A'..=b'Z' |
b'^' |
b'_' |
b'`' |
b'a'..=b'z' |
b'{' |
b'|' |
b'}' |
b'~' |
128..=255)
}
fn write_quoted_string_char(f: &mut Formatter<'_>, c: char) -> FmtResult {
match c {
'\n' | '\r' => Err(std::fmt::Error),
'\t' | ' ' => f.write_char(c),
c if match c as u32 {
1..=8 | 11 | 12 | 14..=31 | 127 |
33 |
35..=91 |
93..=126 |
128.. => true,
_ => false,
} =>
{
f.write_char(c)
}
_ => {
f.write_char('\\')?;
f.write_char(c)
}
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::Mailbox;
#[test]
fn mailbox_format_address_only() {
assert_eq!(
format!(
"{}",
Mailbox::new(None, "kayo@example.com".parse().unwrap())
),
"kayo@example.com"
);
}
#[test]
fn mailbox_format_address_with_name() {
assert_eq!(
format!(
"{}",
Mailbox::new(Some("K.".into()), "kayo@example.com".parse().unwrap())
),
"\"K.\" <kayo@example.com>"
);
}
#[test]
fn mailbox_format_address_with_comma() {
assert_eq!(
format!(
"{}",
Mailbox::new(
Some("Last, First".into()),
"kayo@example.com".parse().unwrap()
)
),
r#""Last, First" <kayo@example.com>"#
);
}
#[test]
fn mailbox_format_address_with_comma_and_non_ascii() {
assert_eq!(
format!(
"{}",
Mailbox::new(
Some("LaÅŸt, First".into()),
"kayo@example.com".parse().unwrap()
)
),
r#""LaÅŸt, First" <kayo@example.com>"#
);
}
#[test]
fn mailbox_format_address_with_comma_and_quoted_non_ascii() {
assert_eq!(
format!(
"{}",
Mailbox::new(
Some(r#"LaÅŸt, "First""#.into()),
"kayo@example.com".parse().unwrap()
)
),
r#""LaÅŸt, \"First\"" <kayo@example.com>"#
);
}
#[test]
fn mailbox_format_address_with_color() {
assert_eq!(
format!(
"{}",
Mailbox::new(
Some("Chris's Wiki :: blog".into()),
"kayo@example.com".parse().unwrap()
)
),
r#""Chris's Wiki :: blog" <kayo@example.com>"#
);
}
#[test]
fn format_address_with_empty_name() {
assert_eq!(
format!(
"{}",
Mailbox::new(Some("".to_owned()), "kayo@example.com".parse().unwrap())
),
"kayo@example.com"
);
}
#[test]
fn format_address_with_name_trim() {
assert_eq!(
format!(
"{}",
Mailbox::new(Some(" K. ".into()), "kayo@example.com".parse().unwrap())
),
"\"K.\" <kayo@example.com>"
);
}
#[test]
fn parse_address_only() {
assert_eq!(
"kayo@example.com".parse(),
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
);
}
#[test]
fn parse_address_only_trim() {
assert_eq!(
" kayo@example.com ".parse(),
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
);
}
#[test]
fn parse_address_with_name() {
assert_eq!(
"K. <kayo@example.com>".parse(),
Ok(Mailbox::new(
Some("K.".into()),
"kayo@example.com".parse().unwrap()
))
);
}
#[test]
fn parse_address_with_name_trim() {
assert_eq!(
" K. <kayo@example.com> ".parse(),
Ok(Mailbox::new(
Some("K.".into()),
"kayo@example.com".parse().unwrap()
))
);
}
#[test]
fn parse_address_with_empty_name() {
assert_eq!(
"<kayo@example.com>".parse(),
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
);
}
#[test]
fn parse_address_with_empty_name_trim() {
assert_eq!(
" <kayo@example.com> ".parse(),
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
);
}
#[test]
fn parse_address_from_tuple() {
assert_eq!(
("K.".to_owned(), "kayo@example.com".to_owned()).try_into(),
Ok(Mailbox::new(
Some("K.".into()),
"kayo@example.com".parse().unwrap()
))
);
}
#[test]
fn parse_quoted_display_name_preserves_spaces() {
assert_eq!(
"\"Last, First\" <kayo@example.com>".parse(),
Ok(Mailbox::new(
Some("Last, First".into()),
"kayo@example.com".parse().unwrap()
))
);
}
}