use minicbor::{CborLen, Decode, Encode};
use ockam_core::errcode::{Kind, Origin};
use ockam_core::{Error, Result};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
use std::hash::Hash;
#[derive(Debug, Clone, Eq, Ord, PartialOrd, Deserialize, Encode, Decode, CborLen, Serialize)]
#[cbor(transparent)]
#[serde(transparent)]
pub struct EmailAddress(#[n(0)] String);
impl EmailAddress {
pub fn new_unsafe(s: &str) -> EmailAddress {
EmailAddress(s.to_string())
}
pub fn parse(s: &str) -> Result<EmailAddress> {
let regex = Regex::new(r"^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$").map_err(Self::incorrect_regex)?;
if regex.is_match(s) {
Ok(EmailAddress(s.to_string()))
} else {
Err(Error::new(
Origin::Api,
Kind::Invalid,
format!("{s} is not a valid email address"),
))
}
}
fn incorrect_regex(e: regex::Error) -> Error {
Error::new(
Origin::Api,
Kind::Invalid,
format!("incorrect regular expression {e:?}"),
)
}
}
impl PartialEq for EmailAddress {
fn eq(&self, other: &Self) -> bool {
self.0.to_lowercase() == other.0.to_lowercase()
}
}
impl Hash for EmailAddress {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.to_lowercase().hash(state)
}
}
impl Display for EmailAddress {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl TryFrom<String> for EmailAddress {
type Error = Error;
fn try_from(value: String) -> std::result::Result<Self, Self::Error> {
EmailAddress::parse(value.as_str())
}
}
impl TryFrom<&str> for EmailAddress {
type Error = Error;
fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
EmailAddress::parse(value)
}
}
#[cfg(test)]
mod tests {
use super::*;
use quickcheck::{quickcheck, Arbitrary, Gen, TestResult};
quickcheck! {
fn parse_valid_address(email_address: ValidEmailAddress) -> TestResult {
match EmailAddress::parse(&email_address.0.0) {
Ok(_) => TestResult::passed(),
Err(e) => TestResult::error(format!("{e:?}")),
}
}
fn parse_invalid_address(email_address: InvalidEmailAddress) -> TestResult {
match EmailAddress::parse(&email_address.0.0) {
Ok(_) => TestResult::error(format!("the email address {} should not be valid", email_address.0)),
Err(_) => TestResult::passed(),
}
}
fn email_equality(email_address1: EqualEmailAddress, email_address2: EqualEmailAddress) -> bool {
email_address1.0 == email_address2.0
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
struct EqualEmailAddress(EmailAddress);
impl Arbitrary for EqualEmailAddress {
fn arbitrary(gen: &mut Gen) -> Self {
EqualEmailAddress(EmailAddress(
gen.choose(&[
"test@ockam.io",
"test@ockam.io",
"tEst@ockam.io",
"test@Ockam.io",
"TEST@OCKAM.IO",
])
.unwrap()
.to_string(),
))
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
struct ValidEmailAddress(EmailAddress);
impl Arbitrary for ValidEmailAddress {
fn arbitrary(gen: &mut Gen) -> Self {
ValidEmailAddress(EmailAddress(
gen.choose(&[
"simple@example.com",
"very.common@example.com",
"x@example.com",
"long.email-address-with-hyphens@and.subdomains.example.com",
"user.name+tag+sorting@example.com",
"name/surname@example.com",
"admin@example",
"example@s.example",
"mailhost!username@example.org",
"user%example.com@example.org",
"user-@example.org",
])
.unwrap()
.to_string(),
))
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
struct InvalidEmailAddress(EmailAddress);
impl Arbitrary for InvalidEmailAddress {
fn arbitrary(gen: &mut Gen) -> Self {
InvalidEmailAddress(EmailAddress(
gen.choose(&[
"abc.example.com",
"a@b@c@example.com",
"a\"b(c)d,e:f;g<h>i[j\\k]l@example.com",
"just\"not\"right@example.com",
"this is\"not\\allowed@example.com",
"this\\ still\\\"not\\allowed@example.com",
"i.like.underscores@but_they_are_not_allowed_in_this_part",
])
.unwrap()
.to_string(),
))
}
}
impl Arbitrary for EmailAddress {
fn arbitrary(gen: &mut Gen) -> Self {
EmailAddress(
gen.choose(&["test@ockam.io", "user@yahoo.com", "ceo@google.com"])
.unwrap()
.to_string(),
)
}
}
}