ddgm 0.1.0

ddgm: DuckDuckGo eMail
use crate::DDG_DOMAIN;
use std::fmt;
use std::str;
use std::str::FromStr;

/// A type to represent a duck address. The single field is used to store only the local part of
/// email address, we do not store the domain as we are working with only one domain here.
#[derive(Debug, Clone)]
pub struct DuckAddress(String);

/// An error returned when parsing a `str` into `DuckAddress` using `from_str`
#[derive(Debug, Clone)]
pub struct ParseDuckAddressError;

impl DuckAddress {
    pub fn get_alias(&self) -> &str {
        &self.0
    }
}

impl str::FromStr for DuckAddress {
    type Err = ParseDuckAddressError;

    fn from_str(s: &str) -> Result<Self, ParseDuckAddressError> {
        let idx = match s.find('@') {
            Some(idx) => {
                if !s.ends_with(DDG_DOMAIN) {
                    return Err(ParseDuckAddressError);
                }
                idx
            }
            None => s.len(),
        };
        let s = &s[0..idx];

        // [-A-Za-z0-9] are the only supported characters in duck address
        let is_valid = s.chars().all(|c| c.is_ascii_alphanumeric() || c == '-');
        if is_valid {
            Ok(DuckAddress(s.to_string()))
        } else {
            Err(ParseDuckAddressError)
        }
    }
}

impl TryFrom<String> for DuckAddress {
    type Error = ParseDuckAddressError;

    fn try_from(s: String) -> Result<Self, ParseDuckAddressError> {
        str::FromStr::from_str(&s)
    }
}

impl fmt::Display for DuckAddress {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let duck_email = format!("{}@{}", self.0, DDG_DOMAIN);
        fmt::Display::fmt(&duck_email, f)
    }
}

pub fn parse_duck_address(s: &str) -> Result<DuckAddress, String> {
    DuckAddress::from_str(s).map_err(|_| format!("{s} is not a valid duck address"))
}

/// A type to represent a generic email address.
#[derive(Debug, Clone)]
pub struct EmailAddress {
    pub local_part: String,
    pub domain: String,
}

/// An error returned when parsing a `str` into `EmailAddress` using `from_str`
#[derive(Debug, Clone)]
pub struct ParseEmailAddressError;

impl str::FromStr for EmailAddress {
    type Err = ParseEmailAddressError;

    fn from_str(s: &str) -> Result<Self, ParseEmailAddressError> {
        let idx = match s.find('@') {
            Some(idx) => idx,
            None => return Err(ParseEmailAddressError),
        };
        let local_part = &s[0..idx];
        let domain = &s[idx + 1..s.len()];

        // Ref: <https://www.rfc-editor.org/rfc/rfc5322#section-3.2.3>
        fn is_valid_char(c: char) -> bool {
            c.is_ascii_alphanumeric()
                || c == '-'
                || c == '!'
                || c == '#'
                || c == '$'
                || c == '%'
                || c == '&'
                || c == '*'
                || c == '\''
                || c == '+'
                || c == '-'
                || c == '/'
                || c == '='
                || c == '?'
                || c == '^'
                || c == '_'
                || c == '`'
                || c == '{'
                || c == '|'
                || c == '}'
                || c == '~'
                || c == '.'
        }

        let local_part_valid = local_part.chars().all(is_valid_char);
        let domain_valid = domain.chars().all(is_valid_char);

        if local_part_valid && domain_valid {
            Ok(EmailAddress {
                local_part: local_part.to_string(),
                domain: domain.to_string(),
            })
        } else {
            Err(ParseEmailAddressError)
        }
    }
}

pub fn parse_email_address(s: &str) -> Result<EmailAddress, String> {
    EmailAddress::from_str(s).map_err(|_| format!("{s} is not a valid email address"))
}