1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.messaging.util; 18 19 import com.google.common.base.CharMatcher; 20 21 /** 22 * Parsing the email address 23 */ 24 public final class EmailAddress { 25 private static final CharMatcher ANY_WHITESPACE = CharMatcher.anyOf( 26 " \t\n\r\f\u000B\u0085\u2028\u2029\u200D\uFFEF\uFFFD\uFFFE\uFFFF"); 27 private static final CharMatcher EMAIL_ALLOWED_CHARS = CharMatcher.inRange((char) 0, (char) 31) 28 .or(CharMatcher.is((char) 127)) 29 .or(CharMatcher.anyOf(" @,:<>")) 30 .negate(); 31 32 /** 33 * Helper method that checks whether the input text is valid email address. 34 * TODO: This creates a new EmailAddress object each time 35 * Need to make it more lightweight by pulling out the validation code into a static method. 36 */ isValidEmail(final String emailText)37 public static boolean isValidEmail(final String emailText) { 38 return new EmailAddress(emailText).isValid(); 39 } 40 41 /** 42 * Parses the specified email address. Internationalized addresses are treated as invalid. 43 * 44 * @param emailString A string representing just an email address. It should 45 * not contain any other tokens. <code>"Name<foo@example.org>"</code> won't be valid. 46 */ EmailAddress(final String emailString)47 public EmailAddress(final String emailString) { 48 this(emailString, false); 49 } 50 51 /** 52 * Parses the specified email address. 53 * 54 * @param emailString A string representing just an email address. It should 55 * not contain any other tokens. <code>"Name<foo@example.org>"</code> won't be valid. 56 * @param i18n Accept an internationalized address if it is true. 57 */ EmailAddress(final String emailString, final boolean i18n)58 public EmailAddress(final String emailString, final boolean i18n) { 59 allowI18n = i18n; 60 valid = parseEmail(emailString); 61 } 62 63 /** 64 * Parses the specified email address. Internationalized addresses are treated as invalid. 65 * 66 * @param user A string representing the username in the email prior to the '@' symbol 67 * @param host A string representing the host following the '@' symbol 68 */ EmailAddress(final String user, final String host)69 public EmailAddress(final String user, final String host) { 70 this(user, host, false); 71 } 72 73 /** 74 * Parses the specified email address. 75 * 76 * @param user A string representing the username in the email prior to the '@' symbol 77 * @param host A string representing the host following the '@' symbol 78 * @param i18n Accept an internationalized address if it is true. 79 */ EmailAddress(final String user, final String host, final boolean i18n)80 public EmailAddress(final String user, final String host, final boolean i18n) { 81 allowI18n = i18n; 82 this.user = user; 83 setHost(host); 84 } 85 parseEmail(final String emailString)86 protected boolean parseEmail(final String emailString) { 87 // check for null 88 if (emailString == null) { 89 return false; 90 } 91 92 // Check for an '@' character. Get the last one, in case the local part is 93 // quoted. See http://b/1944742. 94 final int atIndex = emailString.lastIndexOf('@'); 95 if ((atIndex <= 0) || // no '@' character in the email address 96 // or @ on the first position 97 (atIndex == (emailString.length() - 1))) { // last character, no host 98 return false; 99 } 100 101 user = emailString.substring(0, atIndex); 102 host = emailString.substring(atIndex + 1); 103 104 return isValidInternal(); 105 } 106 107 @Override toString()108 public String toString() { 109 return user + "@" + host; 110 } 111 112 /** 113 * Ensure the email address is valid, conforming to current RFC2821 and 114 * RFC2822 guidelines (although some iffy characters, like ! and ;, are 115 * allowed because they are not technically prohibited in the RFC) 116 */ isValidInternal()117 private boolean isValidInternal() { 118 if ((user == null) || (host == null)) { 119 return false; 120 } 121 122 if ((user.length() == 0) || (host.length() == 0)) { 123 return false; 124 } 125 126 // check for white space in the host 127 if (ANY_WHITESPACE.indexIn(host) >= 0) { 128 return false; 129 } 130 131 // ensure the host is above the minimum length 132 if (host.length() < 4) { 133 return false; 134 } 135 136 final int firstDot = host.indexOf('.'); 137 138 // ensure host contains at least one dot 139 if (firstDot == -1) { 140 return false; 141 } 142 143 // check if the host contains two continuous dots. 144 if (host.indexOf("..") >= 0) { 145 return false; 146 } 147 148 // check if the first host char is a dot. 149 if (host.charAt(0) == '.') { 150 return false; 151 } 152 153 final int secondDot = host.indexOf(".", firstDot + 1); 154 155 // if there's a dot at the end, there needs to be a second dot 156 if (host.charAt(host.length() - 1) == '.' && secondDot == -1) { 157 return false; 158 } 159 160 // Host must not have any disallowed characters; allowI18n dictates whether 161 // host must be ASCII. 162 if (!EMAIL_ALLOWED_CHARS.matchesAllOf(host) 163 || (!allowI18n && !CharMatcher.ascii().matchesAllOf(host))) { 164 return false; 165 } 166 167 if (user.startsWith("\"")) { 168 if (!isQuotedUserValid()) { 169 return false; 170 } 171 } else { 172 // check for white space in the user 173 if (ANY_WHITESPACE.indexIn(user) >= 0) { 174 return false; 175 } 176 177 // the user cannot contain two continuous dots 178 if (user.indexOf("..") >= 0) { 179 return false; 180 } 181 182 // User must not have any disallowed characters; allow I18n dictates whether 183 // user must be ASCII. 184 if (!EMAIL_ALLOWED_CHARS.matchesAllOf(user) 185 || (!allowI18n && !CharMatcher.ascii().matchesAllOf(user))) { 186 return false; 187 } 188 } 189 return true; 190 } 191 isQuotedUserValid()192 private boolean isQuotedUserValid() { 193 final int limit = user.length() - 1; 194 if (limit < 1 || !user.endsWith("\"")) { 195 return false; 196 } 197 198 // Unusual loop bounds (looking only at characters between the outer quotes, 199 // not at either quote character). Plus, i is manipulated within the loop. 200 for (int i = 1; i < limit; ++i) { 201 final char ch = user.charAt(i); 202 if (ch == '"' || ch == 127 203 // No non-whitespace control chars: 204 || (ch < 32 && !ANY_WHITESPACE.matches(ch)) 205 // No non-ASCII chars, unless i18n is in effect: 206 || (ch >= 128 && !allowI18n)) { 207 return false; 208 } else if (ch == '\\') { 209 if (i + 1 < limit) { 210 ++i; // Skip the quoted character 211 } else { 212 // We have a trailing backslash -- so it can't be quoting anything. 213 return false; 214 } 215 } 216 } 217 218 return true; 219 } 220 221 @Override equals(final Object otherObject)222 public boolean equals(final Object otherObject) { 223 // Do an instance check first as an optimization. 224 if (this == otherObject) { 225 return true; 226 } 227 if (otherObject instanceof EmailAddress) { 228 final EmailAddress otherAddress = (EmailAddress) otherObject; 229 return toString().equals(otherAddress.toString()); 230 } 231 return false; 232 } 233 234 @Override hashCode()235 public int hashCode() { 236 // Arbitrary hash code as a function of both host and user. 237 return toString().hashCode(); 238 } 239 240 // accessors isValid()241 public boolean isValid() { 242 return valid; 243 } 244 getUser()245 public String getUser() { 246 return user; 247 } 248 getHost()249 public String getHost() { 250 return host; 251 } 252 253 // used to change the host on an email address and rechecks validity 254 255 /** 256 * Changes the host name of the email address and rechecks the address' 257 * validity. Exercise caution when storing EmailAddress instances in 258 * hash-keyed collections. Calling setHost() with a different host name will 259 * change the return value of hashCode. 260 * 261 * @param hostName The new host name of the email address. 262 */ setHost(final String hostName)263 public void setHost(final String hostName) { 264 host = hostName; 265 valid = isValidInternal(); 266 } 267 268 protected boolean valid = false; 269 protected String user = null; 270 protected String host = null; 271 protected boolean allowI18n = false; 272 } 273