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 * Perform email validations.
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{Cc}\\(\\)<>@,;:'\\\\\\\"\\.\\[\\]";
040    private static final String VALID_CHARS = "(\\\\[^\\p{Cc}])|[^\\s" + SPECIAL_CHARS + "]";
041    private static final String QUOTED_USER = "(\"(\\\\\"|[^\"\\p{Cc}])*\")";
042    private static final String WORD = "((" + VALID_CHARS + "|')+|" + QUOTED_USER + ")";
043
044    private static final String EMAIL_REGEX = "^(.+)@(\\S+)$";
045
046    /**
047     * RFC 5321 section 4.1.3: an IPv6 address literal carries the "IPv6:" tag (case-insensitive), an IPv4 literal is untagged.
048     */
049    private static final String IP_DOMAIN_REGEX = "^\\[((?i)IPv6:)?(.*)\\]$";
050    private static final String USER_REGEX = "^" + WORD + "(\\." + WORD + ")*$";
051
052    private static final Pattern EMAIL_PATTERN = Pattern.compile(EMAIL_REGEX);
053    private static final Pattern IP_DOMAIN_PATTERN = Pattern.compile(IP_DOMAIN_REGEX);
054    private static final Pattern USER_PATTERN = Pattern.compile(USER_REGEX);
055
056    private static final int MAX_USERNAME_LEN = 64;
057
058    /**
059     * Singleton instance of this class, which
060     *  doesn't consider local addresses as valid.
061     */
062    private static final EmailValidator EMAIL_VALIDATOR = new EmailValidator(false, false);
063
064    /**
065     * Singleton instance of this class, which
066     *  doesn't consider local addresses as valid.
067     */
068    private static final EmailValidator EMAIL_VALIDATOR_WITH_TLD = new EmailValidator(false, true);
069
070    /**
071     * Singleton instance of this class, which does
072     *  consider local addresses valid.
073     */
074    private static final EmailValidator EMAIL_VALIDATOR_WITH_LOCAL = new EmailValidator(true, false);
075
076    /**
077     * Singleton instance of this class, which does
078     *  consider local addresses valid.
079     */
080    private static final EmailValidator EMAIL_VALIDATOR_WITH_LOCAL_WITH_TLD = new EmailValidator(true, true);
081
082    /**
083     * Returns the Singleton instance of this validator.
084     *
085     * @return singleton instance of this validator.
086     */
087    public static EmailValidator getInstance() {
088        return EMAIL_VALIDATOR;
089    }
090
091    /**
092     * Returns the Singleton instance of this validator,
093     *  with local validation as required.
094     *
095     * @param allowLocal Should local addresses be considered valid?
096     * @return singleton instance of this validator
097     */
098    public static EmailValidator getInstance(final boolean allowLocal) {
099        return getInstance(allowLocal, false);
100    }
101
102    /**
103     * Returns the Singleton instance of this validator, with local validation as required.
104     *
105     * @param allowLocal Should local addresses be considered valid?
106     * @param allowTld   Should TLDs be allowed?
107     * @return singleton instance of this validator
108     */
109    public static EmailValidator getInstance(final boolean allowLocal, final boolean allowTld) {
110        if (allowLocal) {
111            if (allowTld) {
112                return EMAIL_VALIDATOR_WITH_LOCAL_WITH_TLD;
113            }
114            return EMAIL_VALIDATOR_WITH_LOCAL;
115        }
116        if (allowTld) {
117            return EMAIL_VALIDATOR_WITH_TLD;
118        }
119        return EMAIL_VALIDATOR;
120    }
121
122    /**
123     * Whether to allow TLDs.
124     */
125    private final boolean allowTld;
126
127    /**
128     * The domain validator.
129     */
130    private final DomainValidator domainValidator;
131
132    /**
133     * Protected constructor for subclasses to use.
134     *
135     * @param allowLocal Should local addresses be considered valid?
136     */
137    protected EmailValidator(final boolean allowLocal) {
138        this(allowLocal, false);
139    }
140
141    /**
142     * Protected constructor for subclasses to use.
143     *
144     * @param allowLocal Should local addresses be considered valid?
145     * @param allowTld Should TLDs be allowed?
146     */
147    protected EmailValidator(final boolean allowLocal, final boolean allowTld) {
148        this.allowTld = allowTld;
149        this.domainValidator = DomainValidator.getInstance(allowLocal);
150    }
151
152    /**
153     * constructor for creating instances with the specified domainValidator
154     *
155     * @param allowLocal Should local addresses be considered valid?
156     * @param allowTld Should TLDs be allowed?
157     * @param domainValidator allow override of the DomainValidator.
158     * The instance must have the same allowLocal setting.
159     * @since 1.7
160     */
161    public EmailValidator(final boolean allowLocal, final boolean allowTld, final DomainValidator domainValidator) {
162        this.allowTld = allowTld;
163        if (domainValidator == null) {
164            throw new IllegalArgumentException("DomainValidator cannot be null");
165        }
166        if (domainValidator.isAllowLocal() != allowLocal) {
167            throw new IllegalArgumentException("DomainValidator must agree with allowLocal setting");
168        }
169        this.domainValidator = domainValidator;
170    }
171
172    /**
173     * Checks if a field has a valid e-mail address.
174     *
175     * @param email The value validation is being performed on.  A {@code null}
176     *              value is considered invalid.
177     * @return true if the email address is valid.
178     */
179    public boolean isValid(final String email) {
180        if (email == null || email.endsWith(".")) { // check this first - it's cheap!
181            return false;
182        }
183        // Check the whole email address structure
184        final Matcher emailMatcher = EMAIL_PATTERN.matcher(email);
185        if (!emailMatcher.matches() || !isValidUser(emailMatcher.group(1)) || !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 = InetAddressValidator.getInstance();
203            if (ipDomainMatcher.group(1) != null) {
204                return inetAddressValidator.isValidInet6Address(ipDomainMatcher.group(2));
205            }
206            return inetAddressValidator.isValidInet4Address(ipDomainMatcher.group(2));
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}