use crate::{EmailAddress, Error};
use std::str::FromStr;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "sqlx")]
use sqlx::{Database, Decode, Encode, Type, encode::IsNull, error::BoxDynError};
#[cfg(feature = "sqlx_postgres")]
use sqlx::postgres::{PgHasArrayType, PgTypeInfo};
const DISPLAY_START: char = '<';
const DISPLAY_END: char = '>';
#[derive(Debug, Clone)]
pub struct Mailbox {
name: Option<String>,
email: EmailAddress,
}
impl Mailbox {
pub fn new(name: Option<String>, email: EmailAddress) -> Self {
Self { name, email }
}
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
pub fn email(&self) -> &EmailAddress {
&self.email
}
pub fn into_address(self) -> EmailAddress {
self.email
}
pub fn into_parts(self) -> (Option<String>, EmailAddress) {
(self.name, self.email)
}
pub fn to_display(&self) -> String {
match &self.name {
Some(name) => format!("{} <{}>", name, self.email),
None => format!("{}", self.email),
}
}
}
impl FromStr for Mailbox {
type Err = Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let s = s.trim();
let (display, email) = match s.rsplit_once(DISPLAY_START) {
None => (None, s),
Some((display, email)) => {
let email = email
.strip_suffix(DISPLAY_END)
.ok_or(Error::MissingEndBracket)?;
let display = display.trim_end();
let display = if display.is_empty() {
None
} else {
Some(display)
};
(display, email)
}
};
Ok(Mailbox::new(
display.map(|s| s.to_owned()),
EmailAddress::from_str(email)?,
))
}
}
impl PartialEq for Mailbox {
fn eq(&self, other: &Self) -> bool {
self.email == other.email
}
}
impl Eq for Mailbox {}
impl PartialOrd for Mailbox {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.email.cmp(&other.email))
}
}
impl Ord for Mailbox {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.email.cmp(&other.email)
}
}
impl std::hash::Hash for Mailbox {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.email.hash(state);
}
}
impl std::fmt::Display for Mailbox {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.to_display())
}
}
impl From<EmailAddress> for Mailbox {
fn from(value: EmailAddress) -> Self {
Self::new(None, value)
}
}
impl From<Mailbox> for EmailAddress {
fn from(value: Mailbox) -> Self {
value.email
}
}
#[cfg(feature = "lettre")]
impl TryFrom<Mailbox> for lettre::message::Mailbox {
type Error = lettre::address::AddressError;
fn try_from(value: Mailbox) -> std::result::Result<Self, Self::Error> {
Ok(Self::new(value.name, value.email.try_into()?))
}
}
#[cfg(feature = "serde")]
impl Serialize for Mailbox {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_display())
}
}
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for Mailbox {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::{Error, Unexpected, Visitor};
use std::{fmt::Formatter, str::FromStr};
struct MailboxVisitor;
impl Visitor<'_> for MailboxVisitor {
type Value = Mailbox;
fn expecting(&self, fmt: &mut Formatter<'_>) -> std::fmt::Result {
fmt.write_str("string containing a valid name-addr")
}
fn visit_str<E>(self, s: &str) -> std::result::Result<Self::Value, E>
where
E: Error,
{
Mailbox::from_str(s).map_err(|err| {
let exp = format!("{}", err);
Error::invalid_value(Unexpected::Str(s), &exp.as_ref())
})
}
}
deserializer.deserialize_str(MailboxVisitor)
}
}
#[cfg(feature = "sqlx")]
impl<DB: Database> Type<DB> for Mailbox
where
String: Type<DB>,
{
fn type_info() -> DB::TypeInfo {
<String as Type<DB>>::type_info()
}
fn compatible(ty: &DB::TypeInfo) -> bool {
<String as Type<DB>>::compatible(ty)
}
}
#[cfg(feature = "sqlx")]
impl<'a, DB: Database> Encode<'a, DB> for Mailbox
where
String: Encode<'a, DB>,
{
fn encode_by_ref(
&self,
buf: &mut <DB as Database>::ArgumentBuffer<'a>,
) -> std::result::Result<IsNull, BoxDynError> {
<String as Encode<'a, DB>>::encode_by_ref(&self.to_display(), buf)
}
fn produces(&self) -> Option<DB::TypeInfo> {
<String as Encode<'a, DB>>::produces(&self.to_display())
}
fn size_hint(&self) -> usize {
<String as Encode<'a, DB>>::size_hint(&self.to_display())
}
}
#[cfg(feature = "sqlx")]
impl<'a, DB: Database> Decode<'a, DB> for Mailbox
where
String: Decode<'a, DB>,
{
fn decode(value: <DB as Database>::ValueRef<'a>) -> std::result::Result<Self, BoxDynError> {
Ok(Self::from_str(&<String as Decode<'a, DB>>::decode(value)?)?)
}
}
#[cfg(feature = "sqlx_postgres")]
impl PgHasArrayType for Mailbox
where
String: PgHasArrayType,
{
fn array_type_info() -> PgTypeInfo {
<String as PgHasArrayType>::array_type_info()
}
}
#[cfg(feature = "sqlx")]
#[cfg(test)]
mod sqlx_tests {
use super::*;
use claims::{assert_err, assert_matches, assert_ok, assert_ok_eq};
use sqlx::{
Any, Decode, Encode, Value,
any::{AnyArguments, AnyValue},
};
use sqlx_core::any::AnyValueKind;
use std::borrow::Cow;
#[test]
fn test_encode() {
let mb = assert_ok!(Mailbox::from_str("Alice <alice@example.com>"));
let mut buf = AnyArguments::default().values;
let _ = assert_ok!(<Mailbox as Encode<'_, Any>>::encode(mb, &mut buf));
assert_eq!(buf.0.len(), 1);
assert_matches!(&buf.0[0], AnyValueKind::Text(text) if text == "Alice <alice@example.com>");
}
#[test]
fn test_decode() {
let value = AnyValue {
kind: AnyValueKind::Text(Cow::from("Alice <alice@example.com>")),
};
let email = assert_ok!(Mailbox::from_str("Alice <alice@example.com>"));
assert_ok_eq!(<Mailbox as Decode<'_, Any>>::decode(value.as_ref()), email);
}
#[test]
fn test_decode_invalid_value() {
let value = AnyValue {
kind: AnyValueKind::Text(Cow::from("Alice <alice@example.org")),
};
let boxed_error = assert_err!(<Mailbox as Decode<'_, Any>>::decode(value.as_ref()));
let error = assert_ok!(boxed_error.downcast::<Error>());
assert_matches!(*error, Error::MissingEndBracket);
}
#[test]
fn test_decode_invalid_type() {
let value = AnyValue {
kind: AnyValueKind::Integer(42),
};
let boxed_error = assert_err!(<Mailbox as Decode<'_, Any>>::decode(value.as_ref()));
assert_eq!(boxed_error.to_string(), "expected TEXT, got Integer(42)");
}
}
#[cfg(feature = "serde")]
#[cfg(test)]
mod serde_tests {
use super::*;
use claims::assert_err_eq;
use serde::de::{Error as _, Unexpected};
use serde_assert::{Deserializer, Token};
#[test]
fn test_roundtrip_00() {
let mb = Mailbox::from_str("Alice <alice@example.org>").unwrap();
let ser = serde_json::to_string(&mb).unwrap();
let de = serde_json::from_str::<Mailbox>(&ser).unwrap();
assert_eq!(mb.name(), de.name());
assert_eq!(mb.email(), de.email());
}
#[test]
fn test_roundtrip_01() {
let mb = Mailbox::from_str("alice@example.org").unwrap();
let ser = serde_json::to_string(&mb).unwrap();
let de = serde_json::from_str::<Mailbox>(&ser).unwrap();
assert_eq!(mb.name(), de.name());
assert_eq!(mb.email(), de.email());
}
#[test]
fn test_serialize() {
let mb = Mailbox::from_str("Alice <alice@example.org>").unwrap();
let ser = serde_json::to_string(&mb).unwrap();
assert_eq!(ser, "\"Alice <alice@example.org>\"");
}
#[test]
fn test_deserialize() {
let ser = "\"<john.doe@example.org>\"";
let de = serde_json::from_str::<Mailbox>(&ser).unwrap();
assert_eq!(de.to_string(), "john.doe@example.org");
}
#[test]
fn test_deserialize_invalid_value() {
let mut deserializer =
Deserializer::builder([Token::Str("Alice <alice@example.org".to_owned())]).build();
assert_err_eq!(
Mailbox::deserialize(&mut deserializer),
serde_assert::de::Error::invalid_value(
Unexpected::Str("Alice <alice@example.org"),
&"End bracket is missing"
)
);
}
#[test]
fn test_deserialize_invalid_type() {
let mut deserializer = Deserializer::builder([Token::U64(42)]).build();
assert_err_eq!(
Mailbox::deserialize(&mut deserializer),
serde_assert::de::Error::invalid_type(
Unexpected::Unsigned(42),
&"string containing a valid name-addr"
)
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_comparisons() {
let a = Mailbox::from_str("Alice <alice@example.org>").unwrap();
let b = Mailbox::from_str("alice <alice@EXAMPLE.org>").unwrap();
assert_eq!(a, b);
}
#[test]
fn test_good_example_00() {
let mb = Mailbox::from_str("Alice <alice@example.org>").unwrap();
assert_eq!(mb.name(), Some("Alice"));
assert_eq!(mb.email().to_string(), "alice@example.org");
}
#[test]
fn test_good_example_01() {
let mb = Mailbox::from_str("alice@example.org").unwrap();
assert_eq!(mb.name(), None);
assert_eq!(mb.email().to_string(), "alice@example.org");
}
#[test]
fn test_good_example_02() {
let mb = Mailbox::from_str("\"John Doe\" <john@example.org>").unwrap();
assert_eq!(mb.name(), Some("\"John Doe\""));
assert_eq!(mb.email().to_string(), "john@example.org");
}
#[test]
fn test_good_example_03() {
let mb = Mailbox::from_str("<alice@example.org>").unwrap();
assert_eq!(mb.name(), None);
assert_eq!(mb.email().to_string(), "alice@example.org");
}
#[test]
fn test_good_example_04() {
let mb = Mailbox::from_str("Doe, Jane <alice@example.org>").unwrap();
assert_eq!(mb.name(), Some("Doe, Jane"));
assert_eq!(mb.email().to_string(), "alice@example.org");
}
#[test]
fn test_good_example_05() {
let mb = Mailbox::from_str("Alice <alice@example.org");
assert!(matches!(mb, Err(Error::MissingEndBracket)));
}
#[test]
fn test_good_example_06() {
let mb = Mailbox::from_str("Less < More <test@example.org>").unwrap();
assert_eq!(mb.name(), Some("Less < More"));
assert_eq!(mb.email().to_string(), "test@example.org");
}
#[test]
fn test_good_example_07() {
let m1 = Mailbox::new(
Some("Alice".to_string()),
EmailAddress::from_str("a@b.com").unwrap(),
);
let m2 = Mailbox::new(
Some("Bob".to_string()),
EmailAddress::from_str("a@b.com").unwrap(),
);
assert_eq!(m1, m2);
}
#[test]
fn test_good_example_08() {
let mb = Mailbox::from_str("Alice<alice@example.org>").unwrap();
assert_eq!(mb.name(), Some("Alice"));
assert_eq!(mb.email().to_string(), "alice@example.org");
}
#[test]
fn test_good_example_09() {
let mb = Mailbox::from_str(" Alice <alice@example.org> ").unwrap();
assert_eq!(mb.name(), Some("Alice"));
assert_eq!(mb.email().to_string(), "alice@example.org");
}
#[test]
fn test_good_example_10() {
let mb = Mailbox::from_str(" <alice@example.org> ").unwrap();
assert_eq!(mb.name(), None);
assert_eq!(mb.email().to_string(), "alice@example.org");
}
#[test]
fn test_good_example_11() {
let mb = Mailbox::from_str("\"John Doe\" <john@example.org>").unwrap();
assert_eq!(mb.name(), Some("\"John Doe\""));
assert_eq!(mb.email().to_string(), "john@example.org");
}
#[test]
fn test_good_example_12() {
let mb = Mailbox::from_str("user+tag <a@b.com>").unwrap();
assert_eq!(mb.name(), Some("user+tag"));
let mb = Mailbox::from_str("A_B-C.D <a@b.com>").unwrap();
assert_eq!(mb.name(), Some("A_B-C.D"));
}
#[test]
fn test_bad_example_00() {
assert!(Mailbox::from_str("Alice <alice@example.org").is_err());
assert!(Mailbox::from_str("Alice alice@example.org>").is_err());
assert!(Mailbox::from_str("Alice <alice@example.org>>").is_err());
}
#[test]
fn test_bad_example_01() {
assert!(Mailbox::from_str("Alice <alice@example.org> trailing").is_err());
}
#[test]
fn test_display() {
let mb = Mailbox::from_str("Alice <alice@example.org>").unwrap();
assert_eq!(format!("{}", mb), "Alice <alice@example.org>");
let mb_no_name = Mailbox::from_str("alice@example.org").unwrap();
assert_eq!(format!("{}", mb_no_name), "alice@example.org");
}
#[test]
fn test_round_trip() {
let original = "Alice <alice@example.org>";
let parsed = Mailbox::from_str(original).unwrap();
let rendered = parsed.to_string();
let reparsed = Mailbox::from_str(&rendered).unwrap();
assert_eq!(parsed, reparsed);
}
#[test]
fn test_hash_consistency() {
use std::collections::HashSet;
let mut set = HashSet::new();
let a = Mailbox::from_str("Alice <a@b.com>").unwrap();
let b = Mailbox::from_str("Bob <a@b.com>").unwrap();
set.insert(a);
set.insert(b);
assert_eq!(set.len(), 1);
}
}