/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.messaging.util;
import com.google.common.base.CharMatcher;
/**
* Parsing the email address
*/
public final class EmailAddress {
private static final CharMatcher ANY_WHITESPACE = CharMatcher.anyOf(
" \t\n\r\f\u000B\u0085\u2028\u2029\u200D\uFFEF\uFFFD\uFFFE\uFFFF");
private static final CharMatcher EMAIL_ALLOWED_CHARS = CharMatcher.inRange((char) 0, (char) 31)
.or(CharMatcher.is((char) 127))
.or(CharMatcher.anyOf(" @,:<>"))
.negate();
/**
* Helper method that checks whether the input text is valid email address.
* TODO: This creates a new EmailAddress object each time
* Need to make it more lightweight by pulling out the validation code into a static method.
*/
public static boolean isValidEmail(final String emailText) {
return new EmailAddress(emailText).isValid();
}
/**
* Parses the specified email address. Internationalized addresses are treated as invalid.
*
* @param emailString A string representing just an email address. It should
* not contain any other tokens. "Name<foo@example.org>"
won't be valid.
*/
public EmailAddress(final String emailString) {
this(emailString, false);
}
/**
* Parses the specified email address.
*
* @param emailString A string representing just an email address. It should
* not contain any other tokens. "Name<foo@example.org>"
won't be valid.
* @param i18n Accept an internationalized address if it is true.
*/
public EmailAddress(final String emailString, final boolean i18n) {
allowI18n = i18n;
valid = parseEmail(emailString);
}
/**
* Parses the specified email address. Internationalized addresses are treated as invalid.
*
* @param user A string representing the username in the email prior to the '@' symbol
* @param host A string representing the host following the '@' symbol
*/
public EmailAddress(final String user, final String host) {
this(user, host, false);
}
/**
* Parses the specified email address.
*
* @param user A string representing the username in the email prior to the '@' symbol
* @param host A string representing the host following the '@' symbol
* @param i18n Accept an internationalized address if it is true.
*/
public EmailAddress(final String user, final String host, final boolean i18n) {
allowI18n = i18n;
this.user = user;
setHost(host);
}
protected boolean parseEmail(final String emailString) {
// check for null
if (emailString == null) {
return false;
}
// Check for an '@' character. Get the last one, in case the local part is
// quoted. See http://b/1944742.
final int atIndex = emailString.lastIndexOf('@');
if ((atIndex <= 0) || // no '@' character in the email address
// or @ on the first position
(atIndex == (emailString.length() - 1))) { // last character, no host
return false;
}
user = emailString.substring(0, atIndex);
host = emailString.substring(atIndex + 1);
return isValidInternal();
}
@Override
public String toString() {
return user + "@" + host;
}
/**
* Ensure the email address is valid, conforming to current RFC2821 and
* RFC2822 guidelines (although some iffy characters, like ! and ;, are
* allowed because they are not technically prohibited in the RFC)
*/
private boolean isValidInternal() {
if ((user == null) || (host == null)) {
return false;
}
if ((user.length() == 0) || (host.length() == 0)) {
return false;
}
// check for white space in the host
if (ANY_WHITESPACE.indexIn(host) >= 0) {
return false;
}
// ensure the host is above the minimum length
if (host.length() < 4) {
return false;
}
final int firstDot = host.indexOf('.');
// ensure host contains at least one dot
if (firstDot == -1) {
return false;
}
// check if the host contains two continuous dots.
if (host.indexOf("..") >= 0) {
return false;
}
// check if the first host char is a dot.
if (host.charAt(0) == '.') {
return false;
}
final int secondDot = host.indexOf(".", firstDot + 1);
// if there's a dot at the end, there needs to be a second dot
if (host.charAt(host.length() - 1) == '.' && secondDot == -1) {
return false;
}
// Host must not have any disallowed characters; allowI18n dictates whether
// host must be ASCII.
if (!EMAIL_ALLOWED_CHARS.matchesAllOf(host)
|| (!allowI18n && !CharMatcher.ascii().matchesAllOf(host))) {
return false;
}
if (user.startsWith("\"")) {
if (!isQuotedUserValid()) {
return false;
}
} else {
// check for white space in the user
if (ANY_WHITESPACE.indexIn(user) >= 0) {
return false;
}
// the user cannot contain two continuous dots
if (user.indexOf("..") >= 0) {
return false;
}
// User must not have any disallowed characters; allow I18n dictates whether
// user must be ASCII.
if (!EMAIL_ALLOWED_CHARS.matchesAllOf(user)
|| (!allowI18n && !CharMatcher.ascii().matchesAllOf(user))) {
return false;
}
}
return true;
}
private boolean isQuotedUserValid() {
final int limit = user.length() - 1;
if (limit < 1 || !user.endsWith("\"")) {
return false;
}
// Unusual loop bounds (looking only at characters between the outer quotes,
// not at either quote character). Plus, i is manipulated within the loop.
for (int i = 1; i < limit; ++i) {
final char ch = user.charAt(i);
if (ch == '"' || ch == 127
// No non-whitespace control chars:
|| (ch < 32 && !ANY_WHITESPACE.matches(ch))
// No non-ASCII chars, unless i18n is in effect:
|| (ch >= 128 && !allowI18n)) {
return false;
} else if (ch == '\\') {
if (i + 1 < limit) {
++i; // Skip the quoted character
} else {
// We have a trailing backslash -- so it can't be quoting anything.
return false;
}
}
}
return true;
}
@Override
public boolean equals(final Object otherObject) {
// Do an instance check first as an optimization.
if (this == otherObject) {
return true;
}
if (otherObject instanceof EmailAddress) {
final EmailAddress otherAddress = (EmailAddress) otherObject;
return toString().equals(otherAddress.toString());
}
return false;
}
@Override
public int hashCode() {
// Arbitrary hash code as a function of both host and user.
return toString().hashCode();
}
// accessors
public boolean isValid() {
return valid;
}
public String getUser() {
return user;
}
public String getHost() {
return host;
}
// used to change the host on an email address and rechecks validity
/**
* Changes the host name of the email address and rechecks the address'
* validity. Exercise caution when storing EmailAddress instances in
* hash-keyed collections. Calling setHost() with a different host name will
* change the return value of hashCode.
*
* @param hostName The new host name of the email address.
*/
public void setHost(final String hostName) {
host = hostName;
valid = isValidInternal();
}
protected boolean valid = false;
protected String user = null;
protected String host = null;
protected boolean allowI18n = false;
}