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: 1741724 $
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     * Returns the Singleton instance of this validator.
077     *
078     * @return singleton instance of this validator.
079     */
080    public static EmailValidator getInstance() {
081        return EMAIL_VALIDATOR;
082    }
083
084    /**
085     * Returns the Singleton instance of this validator,
086     *  with local validation as required.
087     *
088     * @param allowLocal Should local addresses be considered valid?
089     * @param allowTld Should TLDs be allowed?
090     * @return singleton instance of this validator
091     */
092    public static EmailValidator getInstance(boolean allowLocal, boolean allowTld) {
093        if (allowLocal) {
094            return EMAIL_VALIDATOR_WITH_LOCAL;
095        } else {
096            if (allowTld) {
097                return EMAIL_VALIDATOR_WITH_TLD;
098            } else {
099                return EMAIL_VALIDATOR;
100            }
101        }
102    }
103
104    /**
105     * Returns the Singleton instance of this validator,
106     *  with local validation as required.
107     *
108     * @param allowLocal Should local addresses be considered valid?
109     * @return singleton instance of this validator
110     */
111    public static EmailValidator getInstance(boolean allowLocal) {
112        return getInstance(allowLocal, false);
113    }
114
115    /**
116     * Protected constructor for subclasses to use.
117     *
118     * @param allowLocal Should local addresses be considered valid?
119     * @param allowTld Should TLDs be allowed?
120     */
121    protected EmailValidator(boolean allowLocal, boolean allowTld) {
122        super();
123        this.allowLocal = allowLocal;
124        this.allowTld = allowTld;
125    }
126
127    /**
128     * <p>Checks if a field has a valid e-mail address.</p>
129     *
130     * @param email The value validation is being performed on.  A <code>null</code>
131     *              value is considered invalid.
132     * @return true if the email address is valid.
133     */
134    @Override
135    public boolean isValid(String email) {
136        if (email == null) {
137            return false;
138        }
139
140        if (email.endsWith(".")) { // check this first - it's cheap!
141            setErrorMessage(tr("E-mail address is invalid"));
142            return false;
143        }
144
145        // Check the whole email address structure
146        Matcher emailMatcher = EMAIL_PATTERN.matcher(email);
147        if (!emailMatcher.matches()) {
148            setErrorMessage(tr("E-mail address is invalid"));
149            return false;
150        }
151
152        String username = emailMatcher.group(1);
153        if (!isValidUser(username)) {
154            setErrorMessage(tr("E-mail address contains an invalid username: {0}", username));
155            return false;
156        }
157
158        String domain = emailMatcher.group(2);
159        if (!isValidDomain(domain)) {
160            setErrorMessage(tr("E-mail address contains an invalid domain: {0}", domain));
161            return false;
162        }
163
164        return true;
165    }
166
167    @Override
168    public String getValidatorName() {
169        return tr("Email validator");
170    }
171
172    /**
173     * Returns true if the domain component of an email address is valid.
174     *
175     * @param domain being validated, may be in IDN format
176     * @return true if the email address's domain is valid.
177     */
178    protected boolean isValidDomain(String domain) {
179        // see if domain is an IP address in brackets
180        Matcher ipDomainMatcher = IP_DOMAIN_PATTERN.matcher(domain);
181
182        if (ipDomainMatcher.matches()) {
183            InetAddressValidator inetAddressValidator =
184                    InetAddressValidator.getInstance();
185            return inetAddressValidator.isValid(ipDomainMatcher.group(1));
186        }
187        // Domain is symbolic name
188        DomainValidator domainValidator =
189                DomainValidator.getInstance(allowLocal);
190        if (allowTld) {
191            return domainValidator.isValid(domain) || (!domain.startsWith(".") && domainValidator.isValidTld(domain));
192        } else {
193            return domainValidator.isValid(domain);
194        }
195    }
196
197    /**
198     * Returns true if the user component of an email address is valid.
199     *
200     * @param user being validated
201     * @return true if the user name is valid.
202     */
203    protected boolean isValidUser(String user) {
204
205        if (user == null || user.length() > MAX_USERNAME_LEN) {
206            return false;
207        }
208
209        return USER_PATTERN.matcher(user).matches();
210    }
211
212}