/* * 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; }