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 * This class is a Singleton; you can retrieve the instance via the getInstance() method.
028 * </p>
029 * <p>
030 * Based on a script by <a href="mailto:stamhankar@hotmail.com">Sandeep V. Tamhankar</a>
031 * http://javascript.internet.com
032 * </p>
033 * <p>
034 * This implementation is not guaranteed to catch all possible errors in an email address.
035 * For example, an address like nobody@noplace.somedog will pass validator, even though there
036 * is no TLD "somedog"
037 * </p>.
038 *
039 * @version $Revision: 1608584 $ $Date: 2014-07-07 19:54:07 UTC (Mon, 07 Jul 2014) $
040 * @since Validator 1.4
041 */
042public class EmailValidator extends AbstractValidator {
043
044    private static final String SPECIAL_CHARS = "\\p{Cntrl}\\(\\)<>@,;:'\\\\\\\"\\.\\[\\]";
045    private static final String VALID_CHARS = "[^\\s" + SPECIAL_CHARS + "]";
046    private static final String QUOTED_USER = "(\"[^\"]*\")";
047    private static final String WORD = "((" + VALID_CHARS + "|')+|" + QUOTED_USER + ")";
048
049    private static final String LEGAL_ASCII_REGEX = "^\\p{ASCII}+$";
050    private static final String EMAIL_REGEX = "^\\s*?(.+)@(.+?)\\s*$";
051    private static final String IP_DOMAIN_REGEX = "^\\[(.*)\\]$";
052    private static final String USER_REGEX = "^\\s*" + WORD + "(\\." + WORD + ")*$";
053
054    private static final Pattern MATCH_ASCII_PATTERN = Pattern.compile(LEGAL_ASCII_REGEX);
055    private static final Pattern EMAIL_PATTERN = Pattern.compile(EMAIL_REGEX);
056    private static final Pattern IP_DOMAIN_PATTERN = Pattern.compile(IP_DOMAIN_REGEX);
057    private static final Pattern USER_PATTERN = Pattern.compile(USER_REGEX);
058
059    private final boolean allowLocal;
060
061    /**
062     * Singleton instance of this class, which
063     *  doesn't consider local addresses as valid.
064     */
065    private static final EmailValidator EMAIL_VALIDATOR = new EmailValidator(false);
066
067    /**
068     * Singleton instance of this class, which does
069     *  consider local addresses valid.
070     */
071    private static final EmailValidator EMAIL_VALIDATOR_WITH_LOCAL = new EmailValidator(true);
072
073    /**
074     * Returns the Singleton instance of this validator.
075     *
076     * @return singleton instance of this validator.
077     */
078    public static EmailValidator getInstance() {
079        return EMAIL_VALIDATOR;
080    }
081
082    /**
083     * Returns the Singleton instance of this validator,
084     *  with local validation as required.
085     *
086     * @param allowLocal Should local addresses be considered valid?
087     * @return singleton instance of this validator
088     */
089    public static EmailValidator getInstance(boolean allowLocal) {
090        if (allowLocal) {
091           return EMAIL_VALIDATOR_WITH_LOCAL;
092        }
093        return EMAIL_VALIDATOR;
094    }
095
096    /**
097     * Protected constructor for subclasses to use.
098     *
099     * @param allowLocal Should local addresses be considered valid?
100     */
101    protected EmailValidator(boolean allowLocal) {
102        super();
103        this.allowLocal = allowLocal;
104    }
105
106    /**
107     * <p>Checks if a field has a valid e-mail address.</p>
108     *
109     * @param email The value validation is being performed on.  A <code>null</code>
110     *              value is considered invalid.
111     * @return true if the email address is valid.
112     */
113    @Override
114    public boolean isValid(String email) {
115        if (email == null) {
116            return false;
117        }
118
119        Matcher asciiMatcher = MATCH_ASCII_PATTERN.matcher(email);
120        if (!asciiMatcher.matches()) {
121            setErrorMessage(tr("E-mail address contains non-ascii characters"));
122            setFix(email.replaceAll("[^\\p{ASCII}]+", ""));
123            return false;
124        }
125
126        // Check the whole email address structure
127        Matcher emailMatcher = EMAIL_PATTERN.matcher(email);
128        if (!emailMatcher.matches()) {
129            setErrorMessage(tr("E-mail address is invalid"));
130            return false;
131        }
132
133        if (email.endsWith(".")) {
134            setErrorMessage(tr("E-mail address is invalid"));
135            return false;
136        }
137
138        String username = emailMatcher.group(1);
139        if (!isValidUser(username)) {
140            setErrorMessage(tr("E-mail address contains an invalid username: {0}", username));
141            return false;
142        }
143
144        String domain = emailMatcher.group(2);
145        if (!isValidDomain(domain)) {
146            setErrorMessage(tr("E-mail address contains an invalid domain: {0}", domain));
147            return false;
148        }
149
150        return true;
151    }
152
153    /**
154     * Returns true if the domain component of an email address is valid.
155     *
156     * @param domain being validated.
157     * @return true if the email address's domain is valid.
158     */
159    protected boolean isValidDomain(String domain) {
160        // see if domain is an IP address in brackets
161        Matcher ipDomainMatcher = IP_DOMAIN_PATTERN.matcher(domain);
162
163        if (ipDomainMatcher.matches()) {
164            InetAddressValidator inetAddressValidator =
165                    InetAddressValidator.getInstance();
166            return inetAddressValidator.isValid(ipDomainMatcher.group(1));
167        } else {
168            // Domain is symbolic name
169            DomainValidator domainValidator =
170                    DomainValidator.getInstance(allowLocal);
171            return domainValidator.isValid(domain) ||
172                    domainValidator.isValidTld(domain);
173        }
174    }
175
176    /**
177     * Returns true if the user component of an email address is valid.
178     *
179     * @param user being validated
180     * @return true if the user name is valid.
181     */
182    protected boolean isValidUser(String user) {
183        return USER_PATTERN.matcher(user).matches();
184    }
185
186}