001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.validator.routines;
018
019import java.io.Serializable;
020import java.util.regex.Matcher;
021import java.util.regex.Pattern;
022
023/**
024 * <p>Perform email validations.</p>
025 * <p>
026 * Based on a script by <a href="mailto:stamhankar@hotmail.com">Sandeep V. Tamhankar</a>
027 * https://javascript.internet.com
028 * </p>
029 * <p>
030 * This implementation is not guaranteed to catch all possible errors in an email address.
031 * </p>.
032 *
033 * @since 1.4
034 */
035public class EmailValidator implements Serializable {
036
037    private static final long serialVersionUID = 1705927040799295880L;
038
039    private static final String SPECIAL_CHARS = "\\p{Cntrl}\\(\\)<>@,;:'\\\\\\\"\\.\\[\\]";
040    private static final String VALID_CHARS = "(\\\\.)|[^\\s" + SPECIAL_CHARS + "]";
041    private static final String QUOTED_USER = "(\"(\\\\\"|[^\"])*\")";
042    private static final String WORD = "((" + VALID_CHARS + "|')+|" + QUOTED_USER + ")";
043
044    private static final String EMAIL_REGEX = "^(.+)@(\\S+)$";
045    private static final String IP_DOMAIN_REGEX = "^\\[(.*)\\]$";
046    private static final String USER_REGEX = "^" + WORD + "(\\." + WORD + ")*$";
047
048    private static final Pattern EMAIL_PATTERN = Pattern.compile(EMAIL_REGEX);
049    private static final Pattern IP_DOMAIN_PATTERN = Pattern.compile(IP_DOMAIN_REGEX);
050    private static final Pattern USER_PATTERN = Pattern.compile(USER_REGEX);
051
052    private static final int MAX_USERNAME_LEN = 64;
053
054    /**
055     * Singleton instance of this class, which
056     *  doesn't consider local addresses as valid.
057     */
058    private static final EmailValidator EMAIL_VALIDATOR = new EmailValidator(false, false);
059
060    /**
061     * Singleton instance of this class, which
062     *  doesn't consider local addresses as valid.
063     */
064    private static final EmailValidator EMAIL_VALIDATOR_WITH_TLD = new EmailValidator(false, true);
065
066    /**
067     * Singleton instance of this class, which does
068     *  consider local addresses valid.
069     */
070    private static final EmailValidator EMAIL_VALIDATOR_WITH_LOCAL = new EmailValidator(true, false);
071
072    /**
073     * Singleton instance of this class, which does
074     *  consider local addresses valid.
075     */
076    private static final EmailValidator EMAIL_VALIDATOR_WITH_LOCAL_WITH_TLD = new EmailValidator(true, true);
077
078    /**
079     * Returns the Singleton instance of this validator.
080     *
081     * @return singleton instance of this validator.
082     */
083    public static EmailValidator getInstance() {
084        return EMAIL_VALIDATOR;
085    }
086
087    /**
088     * Returns the Singleton instance of this validator,
089     *  with local validation as required.
090     *
091     * @param allowLocal Should local addresses be considered valid?
092     * @return singleton instance of this validator
093     */
094    public static EmailValidator getInstance(final boolean allowLocal) {
095        return getInstance(allowLocal, false);
096    }
097
098    /**
099     * Returns the Singleton instance of this validator,
100     *  with local validation as required.
101     *
102     * @param allowLocal Should local addresses be considered valid?
103     * @param allowTld Should TLDs be allowed?
104     * @return singleton instance of this validator
105     */
106    public static EmailValidator getInstance(final boolean allowLocal, final boolean allowTld) {
107        if (allowLocal) {
108            if (allowTld) {
109                return EMAIL_VALIDATOR_WITH_LOCAL_WITH_TLD;
110            }
111            return EMAIL_VALIDATOR_WITH_LOCAL;
112        }
113        if (allowTld) {
114            return EMAIL_VALIDATOR_WITH_TLD;
115        }
116        return EMAIL_VALIDATOR;
117    }
118
119    private final boolean allowTld;
120
121    private final DomainValidator domainValidator;
122
123    /**
124     * Protected constructor for subclasses to use.
125     *
126     * @param allowLocal Should local addresses be considered valid?
127     */
128    protected EmailValidator(final boolean allowLocal) {
129        this(allowLocal, false);
130    }
131
132    /**
133     * Protected constructor for subclasses to use.
134     *
135     * @param allowLocal Should local addresses be considered valid?
136     * @param allowTld Should TLDs be allowed?
137     */
138    protected EmailValidator(final boolean allowLocal, final boolean allowTld) {
139        this.allowTld = allowTld;
140        this.domainValidator = DomainValidator.getInstance(allowLocal);
141    }
142
143    /**
144     * constructor for creating instances with the specified domainValidator
145     *
146     * @param allowLocal Should local addresses be considered valid?
147     * @param allowTld Should TLDs be allowed?
148     * @param domainValidator allow override of the DomainValidator.
149     * The instance must have the same allowLocal setting.
150     * @since 1.7
151     */
152    public EmailValidator(final boolean allowLocal, final boolean allowTld, final DomainValidator domainValidator) {
153        this.allowTld = allowTld;
154        if (domainValidator == null) {
155            throw new IllegalArgumentException("DomainValidator cannot be null");
156        }
157        if (domainValidator.isAllowLocal() != allowLocal) {
158            throw new IllegalArgumentException("DomainValidator must agree with allowLocal setting");
159        }
160        this.domainValidator = domainValidator;
161    }
162
163    /**
164     * <p>Checks if a field has a valid e-mail address.</p>
165     *
166     * @param email The value validation is being performed on.  A {@code null}
167     *              value is considered invalid.
168     * @return true if the email address is valid.
169     */
170    public boolean isValid(final String email) {
171        if (email == null) {
172            return false;
173        }
174        if (email.endsWith(".")) { // check this first - it's cheap!
175            return false;
176        }
177        // Check the whole email address structure
178        final Matcher emailMatcher = EMAIL_PATTERN.matcher(email);
179        if (!emailMatcher.matches()) {
180            return false;
181        }
182        if (!isValidUser(emailMatcher.group(1))) {
183            return false;
184        }
185        if (!isValidDomain(emailMatcher.group(2))) {
186            return false;
187        }
188        return true;
189    }
190
191    /**
192     * Returns true if the domain component of an email address is valid.
193     *
194     * @param domain being validated, may be in IDN format
195     * @return true if the email address's domain is valid.
196     */
197    protected boolean isValidDomain(final String domain) {
198        // see if domain is an IP address in brackets
199        final Matcher ipDomainMatcher = IP_DOMAIN_PATTERN.matcher(domain);
200
201        if (ipDomainMatcher.matches()) {
202            final InetAddressValidator inetAddressValidator =
203                    InetAddressValidator.getInstance();
204            return inetAddressValidator.isValid(ipDomainMatcher.group(1));
205        }
206        // Domain is symbolic name
207        if (allowTld) {
208            return domainValidator.isValid(domain) || !domain.startsWith(".") && domainValidator.isValidTld(domain);
209        }
210        return domainValidator.isValid(domain);
211    }
212
213    /**
214     * Returns true if the user component of an email address is valid.
215     *
216     * @param user being validated
217     * @return true if the user name is valid.
218     */
219    protected boolean isValidUser(final String user) {
220
221        if (user == null || user.length() > MAX_USERNAME_LEN) {
222            return false;
223        }
224
225        return USER_PATTERN.matcher(user).matches();
226    }
227
228}