/*
 * @(#)EmailAddress.java
 *
 * Copyright (c) 2001-2002 JScape
 * 1147 S. 53rd Pl., Mesa, Arizona, 85206, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of
 * JScape. ("Confidential Information").  You shall not disclose such
 * Confidential Information and shall use it only in accordance with
 * the terms of the license agreement you entered into with JScape.
 */
package com.jscape.inet.email;

import java.io.Serializable;
import java.util.StringTokenizer;
import java.util.regex.Pattern;


/**
 * Implements basic functionality for parsing and validating the syntax of an email address.
 * <p/>
 * Example Usage:
 * <p/>
 * <pre>
 * // create new instance
 * EmailAdress email = new EmailAddress("user@myserver.com");
 *
 * // check for validity
 * System.out.println("Valid: " + email.isValid());
 * </pre>
 */
public class EmailAddress
        implements Serializable {

    // tokens from RFC 2822
    private static final String FWS = "[ \\t]*";
    private static final String NO_WS_CTL = "[\\x01-\\x09\\x0B\\x0C\\x0E-\\x7F]";
    private static final String DQUOTE = "\\\"";
    private static final String ATEXT = "[a-zA-Z0-9\\!\\#\\$\\%\\&\\'\\*\\+\\-\\/\\=\\?\\^\\_\\`\\{\\|\\}\\~]";
    private static final String DTEXT = "[\\x01-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F\\x21-\\x5A\\x5E-\\x7E]";
    private static final String QTEXT = "[\\x01-\\x08\\x0B\\x0C\\x0E-\\x1F\\x7F\\x21\\x23-\\x5B\\x5D-\\x7E]";
    private static final String ATOM = String.format("%s%s+%s", FWS, ATEXT, FWS);
    private static final String DOT_ATOM = String.format("%s(%s+(\\.%s+)*)%s", FWS, ATEXT, ATEXT, FWS);
    private static final String QCONTENT = String.format("(%s|(\\\\%s))", QTEXT, NO_WS_CTL);
    private static final String QUOTED_STRING = String.format("%s(%s%s)*%s%s", DQUOTE, FWS, QCONTENT, FWS, DQUOTE);
    private static final String DOMAIN = "(" + DOT_ATOM + "|\\[(" + FWS + DTEXT + "|(\\\\" + NO_WS_CTL + ")+)*" + FWS + "\\])";
    private static final String DISPLAY_NAME = String.format("(%s|%s)+", ATOM, QUOTED_STRING);
    private static final String ADDR_SPEC = "((" + DOT_ATOM + ")|(" + QUOTED_STRING + "))@" + DOMAIN;
    private static final String NAME_ADDR = "(" + DISPLAY_NAME + ")?" + FWS + "<" + ADDR_SPEC + ">";
    private static final String MAILBOX = NAME_ADDR + "|" + ADDR_SPEC;
    // tokens from RFC 2822

    private static final Pattern PATTERN_NAME_ADDR = Pattern.compile(NAME_ADDR);
    private static final Pattern PATTERN_ADDR_SPEC = Pattern.compile(ADDR_SPEC);
    //private static final Pattern PATTERN_MAILBOX = Pattern.compile(MAILBOX);

    private static final char[] EMAIL_SET = {
            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
            'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b',
            'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
            'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3',
            '4', '5', '6', '7', '8', '9', '@', '-', '.', '_', '\'', '+', '$',
            '/', '%'
    };

    private static final char[] DOMAIN_SET = {
            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
            'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b',
            'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
            'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3',
            '4', '5', '6', '7', '8', '9', '-', '.'
    };

    private static final char[] SUB_DOMAIN_SET = {
            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
            'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b',
            'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
            'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3',
            '4', '5', '6', '7', '8', '9', '-'
    };

    private String email;
    private String user;
    private String domain;
    private String name;
    private boolean valid;

    /**
     * Constructs a new <code>EmailAddress</code>.
     *
     * @param email the email address
     *              Valid email addresses examples:<P>
     *              jsmith@domain.com<br>
     *              &lt;jsmith@domain.com&gt;<br>
     *              John Smith &lt;jsmith@domain.com&gt;<br>
     *              jsmith@domain.com (John Smith)<br>
     */
    public EmailAddress(String email) {
        this.email = email.trim();
        parse();
    }

    /**
     * Checks to see if this email address is syntactically valid.
     *
     * @return boolean <code>true</code> if email address is valid <code>false</code> otherwise
     */
    public boolean isValid() {
        return valid;
    }

    /**
     * Return the full email address in the form user@domain.
     *
     * @return full email adddress
     */
    public String getEmail() {
        return email;
    }

    /**
     * Gets the user portion of the email address.
     *
     * @return user portion of email address
     */
    public String getUser() {
        return user;
    }

    /**
     * Gets the domain portion of the email address.
     *
     * @return domain portion of email address
     */
    public String getDomain() {
        return domain;
    }

    /**
     * Gets the name portion of the email address.
     *
     * @return name portion of email address or <code>null</code> if not availalbe
     */
    public String getName() {
        return name;
    }

    /**
     * Get full email address.
     *
     * @return full email address
     */
    public String toString() {
        return email != null ? name != null ? name + " <" + email + ">" : email : null;
    }

    /**
     * Checks email address for valid syntax.
     */
    private void parse() {
        if (email == null) {
            return;
        }

        if ((email.indexOf("<") != -1) && email.endsWith(">")) {
            // find last index of left arrow
            int lt = email.lastIndexOf("<");

            if (lt > 0) {
                name = email.substring(0, lt).trim();
            }

            email = email.substring(lt + 1, email.length() - 1);
        }

        if ((email.indexOf("(") != -1) && email.endsWith(")")) {
            int lt = email.indexOf("(");

            if (lt > 0) {
                name = email.substring(lt + 1, email.length() - 1);
                email = email.substring(0, lt).trim();
            }
        }

        // Check that email is of minimum length
        if (email.length() < 6) {
            return;
        }

        // Check for @ symbol
        int at = email.indexOf("@");

        if (at == -1) {
            return;
        }

        // Check that email does not contain any invalid chars
//        for (int i = 0; i < email.length(); ++i) {
//            if (!isValidChar(email.charAt(i), EMAIL_SET)) {
//                return;
//            }
//        }
        if (!conformsRfc2822(email)) {
            return;
        }

        // Parse out user and domain
        user = email.substring(0, at);
        domain = email.substring(at + 1);

        valid = parseUser() && parseDomain();
    }

    /**
     * Checks domain portion of email address.
     *
     * @return domain
     */
    private boolean parseDomain() {
        // Domain may not be null
        if ((domain == null) || (domain.length() > 256) || (domain.length() < 4)) {
            return false;
        }

        if (domain.indexOf("..") != -1) {
            return false;
        }

        if (domain.startsWith(".") || domain.endsWith(".")) {
            return false;
        }
        // Check that domain contains valid characters
//        for (int i = 0; i < domain.length(); ++i) {
//            if (!isValidChar(domain.charAt(i), DOMAIN_SET)) {
//                return false;
//            }
//        }

        StringTokenizer st = new StringTokenizer(domain, ".");

        // Must have at least one tld
        if (st.countTokens() < 2) {
            return false;
        }

        // Check the TLD for syntax
        while (st.hasMoreTokens()) {
            String token = st.nextToken();

            if (token.startsWith("-") || token.endsWith("-")) {
                return false;
            }

            for (int i = 0; i < token.length(); ++i) {
                // Last token .. make sure length is at least 2
                if (!st.hasMoreTokens()) {
                    if (token.length() < 2) {
                        return false;
                    }
                }

//                if (!isValidChar(token.charAt(i), SUB_DOMAIN_SET)) {
//                    return false;
//                }
            }
        }

        return true;
    }


    /**
     * Checks user portion of email address.
     *
     * @return true if user format is correct, false otherwise
     */
    private boolean parseUser() {
        // Check to see that user length is correct
        if ((user == null) || (user.length() > 64) || (user.length() < 1)) {
            return false;
        }

        // user cannot start or end with a .
        if (user.startsWith(".") || user.endsWith(".")) {
            return false;
        }

        return true;
    }

    /**
     * Checks if character is part of valid character set.
     *
     * @param c   the character to check
     * @param set the valid character set
     *
     * @return <code>true</code> if character is valid
     */
    private boolean isValidChar(char c, char[] set) {
        boolean found = false;

        for (int i = 0; i < set.length; ++i) {
            if (set[i] == c) {
                found = true;

                break;
            }
        }

        return found;
    }

    private boolean conformsRfc2822(String address) {
        // for long email addresses validation with regex PATTERN_MAILBOX
        // may take too long, dividing by two regular expression,
        // see FTPSERVER-702
        if (address.contains("<") || address.contains(">")) {
            return PATTERN_NAME_ADDR.matcher(address).matches();
        } else {
            return PATTERN_ADDR_SPEC.matcher(address).matches();
        }
    }

}