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.openstreetmap.josm.data.validation.routines;
018
019import static org.openstreetmap.josm.tools.I18n.tr;
020
021import java.util.regex.Matcher;
022import java.util.regex.Pattern;
023
024/**
025 * <p>Perform email validations.</p>
026 * <p>
027 * Based on a script by <a href="mailto:stamhankar@hotmail.com">Sandeep V. Tamhankar</a>
028 * http://javascript.internet.com
029 * </p>
030 * <p>
031 * This implementation is not guaranteed to catch all possible errors in an email address.
032 * </p>.
033 *
034 * @version $Revision: 1723573 $
035 * @since Validator 1.4
036 */
037public class EmailValidator extends AbstractValidator {
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*?(.+)@(.+?)\\s*$";
045    private static final String IP_DOMAIN_REGEX = "^\\[(.*)\\]$";
046    private static final String USER_REGEX = "^\\s*" + 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    private final boolean allowLocal;
055    private final boolean allowTld;
056
057    /**
058     * Singleton instance of this class, which
059     *  doesn't consider local addresses as valid.
060     */
061    private static final EmailValidator EMAIL_VALIDATOR = new EmailValidator(false, false);
062
063    /**
064     * Singleton instance of this class, which
065     *  doesn't consider local addresses as valid.
066     */
067    private static final EmailValidator EMAIL_VALIDATOR_WITH_TLD = new EmailValidator(false, true);
068
069    /**
070     * Singleton instance of this class, which does
071     *  consider local addresses valid.
072     */
073    private static final EmailValidator EMAIL_VALIDATOR_WITH_LOCAL = new EmailValidator(true, false);
074
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     * @param allowTld Should TLDs be allowed?
097     * @return singleton instance of this validator
098     */
099    public static EmailValidator getInstance(boolean allowLocal, boolean allowTld) {
100        if (allowLocal) {
101            if (allowTld) {
102                return EMAIL_VALIDATOR_WITH_LOCAL_WITH_TLD;
103            } else {
104                return EMAIL_VALIDATOR_WITH_LOCAL;
105            }
106        } else {
107            if (allowTld) {
108                return EMAIL_VALIDATOR_WITH_TLD;
109            } else {
110                return EMAIL_VALIDATOR;
111            }
112        }
113    }
114
115    /**
116     * Returns the Singleton instance of this validator,
117     *  with local validation as required.
118     *
119     * @param allowLocal Should local addresses be considered valid?
120     * @return singleton instance of this validator
121     */
122    public static EmailValidator getInstance(boolean allowLocal) {
123        return getInstance(allowLocal, false);
124    }
125
126    /**
127     * Protected constructor for subclasses to use.
128     *
129     * @param allowLocal Should local addresses be considered valid?
130     * @param allowTld Should TLDs be allowed?
131     */
132    protected EmailValidator(boolean allowLocal, boolean allowTld) {
133        super();
134        this.allowLocal = allowLocal;
135        this.allowTld = allowTld;
136    }
137
138    /**
139     * Protected constructor for subclasses to use.
140     *
141     * @param allowLocal Should local addresses be considered valid?
142     */
143    protected EmailValidator(boolean allowLocal) {
144        super();
145        this.allowLocal = allowLocal;
146        this.allowTld = false;
147    }
148
149    /**
150     * <p>Checks if a field has a valid e-mail address.</p>
151     *
152     * @param email The value validation is being performed on.  A <code>null</code>
153     *              value is considered invalid.
154     * @return true if the email address is valid.
155     */
156    @Override
157    public boolean isValid(String email) {
158        if (email == null) {
159            return false;
160        }
161
162        if (email.endsWith(".")) { // check this first - it's cheap!
163            setErrorMessage(tr("E-mail address is invalid"));
164            return false;
165        }
166
167        // Check the whole email address structure
168        Matcher emailMatcher = EMAIL_PATTERN.matcher(email);
169        if (!emailMatcher.matches()) {
170            setErrorMessage(tr("E-mail address is invalid"));
171            return false;
172        }
173
174        String username = emailMatcher.group(1);
175        if (!isValidUser(username)) {
176            setErrorMessage(tr("E-mail address contains an invalid username: {0}", username));
177            return false;
178        }
179
180        String domain = emailMatcher.group(2);
181        if (!isValidDomain(domain)) {
182            setErrorMessage(tr("E-mail address contains an invalid domain: {0}", domain));
183            return false;
184        }
185
186        return true;
187    }
188
189    @Override
190    public String getValidatorName() {
191        return tr("Email validator");
192    }
193
194    /**
195     * Returns true if the domain component of an email address is valid.
196     *
197     * @param domain being validated, may be in IDN format
198     * @return true if the email address's domain is valid.
199     */
200    protected boolean isValidDomain(String domain) {
201        // see if domain is an IP address in brackets
202        Matcher ipDomainMatcher = IP_DOMAIN_PATTERN.matcher(domain);
203
204        if (ipDomainMatcher.matches()) {
205            InetAddressValidator inetAddressValidator =
206                    InetAddressValidator.getInstance();
207            return inetAddressValidator.isValid(ipDomainMatcher.group(1));
208        }
209        // Domain is symbolic name
210        DomainValidator domainValidator =
211                DomainValidator.getInstance(allowLocal);
212        if (allowTld) {
213            return domainValidator.isValid(domain) || (!domain.startsWith(".") && domainValidator.isValidTld(domain));
214        } else {
215            return domainValidator.isValid(domain);
216        }
217    }
218
219    /**
220     * Returns true if the user component of an email address is valid.
221     *
222     * @param user being validated
223     * @return true if the user name is valid.
224     */
225    protected boolean isValidUser(String user) {
226
227        if (user == null || user.length() > MAX_USERNAME_LEN) {
228            return false;
229        }
230
231        return USER_PATTERN.matcher(user).matches();
232    }
233
234}