Documentation
// Copyright 2019 YechaoLi
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use crate::AsValidateStr;
use regex::Regex;

lazy_static! {
    // Regex from the specs
    // https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address
    // It will mark esoteric email addresses like quoted string as invalid
    static ref EMAIL_USER_RE: Regex = Regex::new(r"^(?i)[a-z0-9.!#$%&'*+/=?^_`{|}~-]+\z").unwrap();
    static ref EMAIL_DOMAIN_RE: Regex = Regex::new(
        r"(?i)^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$"
    ).unwrap();
    // literal form, ipv4 or ipv6 address (SMTP 4.1.3)
    static ref EMAIL_LITERAL_RE: Regex = Regex::new(r"(?i)\[([A-f0-9:\.]+)\]\z").unwrap();
}

#[derive(Debug, Clone)]
pub struct Email;

impl Email {
    pub fn validate<'a, T>(&self, val: &'a T) -> bool
    where
        T: AsValidateStr<'a>,
    {
        if let Some(val) = val.as_validate_str().as_ref() {
            if val.is_empty() || !val.contains('@') {
                return false;
            }
            let parts: Vec<&str> = val.rsplitn(2, '@').collect();
            let user_part = parts[1];
            if !EMAIL_USER_RE.is_match(user_part) {
                return false;
            }

            let domain_part = parts[0];
            if !validate_domain_part(domain_part) {
                // Still the possibility of an [IDN](https://en.wikipedia.org/wiki/Internationalized_domain_name)
                return match idna::domain_to_ascii(domain_part) {
                    Ok(d) => validate_domain_part(&d),
                    Err(_) => false,
                };
            }
        }

        true
    }
}

/// Checks if the domain is a valid domain and if not, check whether it's an IP
fn validate_domain_part(domain_part: &str) -> bool {
    if EMAIL_DOMAIN_RE.is_match(domain_part) {
        return true;
    }

    // maybe we have an ip as a domain?
    match EMAIL_LITERAL_RE.captures(domain_part) {
        Some(caps) => match caps.get(1) {
            Some(c) => crate::ip::Ip.validate(&c.as_str()),
            None => false,
        },
        None => false,
    }
}

#[cfg(feature = "proc")]
impl quote::ToTokens for Email {
    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
        use quote::quote;

        tokens.extend(quote! {
            (::prove::email::Email)
        });
    }
}