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 *      https://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, with local validation as required.
100     *
101     * @param allowLocal Should local addresses be considered valid?
102     * @param allowTld   Should TLDs be allowed?
103     * @return singleton instance of this validator
104     */
105    public static EmailValidator getInstance(final boolean allowLocal, final boolean allowTld) {
106        if (allowLocal) {
107            if (allowTld) {
108                return EMAIL_VALIDATOR_WITH_LOCAL_WITH_TLD;
109            }
110            return EMAIL_VALIDATOR_WITH_LOCAL;
111        }
112        if (allowTld) {
113            return EMAIL_VALIDATOR_WITH_TLD;
114        }
115        return EMAIL_VALIDATOR;
116    }
117
118    /**
119     * Whether to allow TLDs.
120     */
121    private final boolean allowTld;
122
123    /**
124     * The domain validator.
125     */
126    private final DomainValidator domainValidator;
127
128    /**
129     * Protected constructor for subclasses to use.
130     *
131     * @param allowLocal Should local addresses be considered valid?
132     */
133    protected EmailValidator(final boolean allowLocal) {
134        this(allowLocal, false);
135    }
136
137    /**
138     * Protected constructor for subclasses to use.
139     *
140     * @param allowLocal Should local addresses be considered valid?
141     * @param allowTld Should TLDs be allowed?
142     */
143    protected EmailValidator(final boolean allowLocal, final boolean allowTld) {
144        this.allowTld = allowTld;
145        this.domainValidator = DomainValidator.getInstance(allowLocal);
146    }
147
148    /**
149     * constructor for creating instances with the specified domainValidator
150     *
151     * @param allowLocal Should local addresses be considered valid?
152     * @param allowTld Should TLDs be allowed?
153     * @param domainValidator allow override of the DomainValidator.
154     * The instance must have the same allowLocal setting.
155     * @since 1.7
156     */
157    public EmailValidator(final boolean allowLocal, final boolean allowTld, final DomainValidator domainValidator) {
158        this.allowTld = allowTld;
159        if (domainValidator == null) {
160            throw new IllegalArgumentException("DomainValidator cannot be null");
161        }
162        if (domainValidator.isAllowLocal() != allowLocal) {
163            throw new IllegalArgumentException("DomainValidator must agree with allowLocal setting");
164        }
165        this.domainValidator = domainValidator;
166    }
167
168    /**
169     * <p>Checks if a field has a valid e-mail address.</p>
170     *
171     * @param email The value validation is being performed on.  A {@code null}
172     *              value is considered invalid.
173     * @return true if the email address is valid.
174     */
175    public boolean isValid(final String email) {
176        if (email == null || email.endsWith(".")) { // check this first - it's cheap!
177            return false;
178        }
179        // Check the whole email address structure
180        final Matcher emailMatcher = EMAIL_PATTERN.matcher(email);
181        if (!emailMatcher.matches()) {
182            return false;
183        }
184        if (!isValidUser(emailMatcher.group(1))) {
185            return false;
186        }
187        if (!isValidDomain(emailMatcher.group(2))) {
188            return false;
189        }
190        return true;
191    }
192
193    /**
194     * Returns true if the domain component of an email address is valid.
195     *
196     * @param domain being validated, may be in IDN format
197     * @return true if the email address's domain is valid.
198     */
199    protected boolean isValidDomain(final String domain) {
200        // see if domain is an IP address in brackets
201        final Matcher ipDomainMatcher = IP_DOMAIN_PATTERN.matcher(domain);
202
203        if (ipDomainMatcher.matches()) {
204            final InetAddressValidator inetAddressValidator =
205                    InetAddressValidator.getInstance();
206            return inetAddressValidator.isValid(ipDomainMatcher.group(1));
207        }
208        // Domain is symbolic name
209        if (allowTld) {
210            return domainValidator.isValid(domain) || !domain.startsWith(".") && domainValidator.isValidTld(domain);
211        }
212        return domainValidator.isValid(domain);
213    }
214
215    /**
216     * Returns true if the user component of an email address is valid.
217     *
218     * @param user being validated
219     * @return true if the username is valid.
220     */
221    protected boolean isValidUser(final String user) {
222
223        if (user == null || user.length() > MAX_USERNAME_LEN) {
224            return false;
225        }
226
227        return USER_PATTERN.matcher(user).matches();
228    }
229
230}