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 package com.android.voicemail.impl.mail; 17 18 import android.os.Parcel; 19 import android.os.Parcelable; 20 import android.support.annotation.VisibleForTesting; 21 import android.text.Html; 22 import android.text.TextUtils; 23 import android.text.util.Rfc822Token; 24 import android.text.util.Rfc822Tokenizer; 25 import com.android.voicemail.impl.mail.utils.LogUtils; 26 import java.util.ArrayList; 27 import java.util.regex.Pattern; 28 import org.apache.james.mime4j.codec.DecodeMonitor; 29 import org.apache.james.mime4j.codec.DecoderUtil; 30 import org.apache.james.mime4j.codec.EncoderUtil; 31 32 /** 33 * This class represent email address. 34 * 35 * <p>RFC822 email address may have following format. "name" <address> (comment) "name" <address> 36 * name <address> address Name and comment part should be MIME/base64 encoded in header if 37 * necessary. 38 */ 39 public class Address implements Parcelable { 40 public static final String ADDRESS_DELIMETER = ","; 41 /** Address part, in the form local_part@domain_part. No surrounding angle brackets. */ 42 private String address; 43 44 /** 45 * Name part. No surrounding double quote, and no MIME/base64 encoding. This must be null if 46 * Address has no name part. 47 */ 48 private String personal; 49 50 /** 51 * When personal is set, it will return the first token of the personal string. Otherwise, it will 52 * return the e-mail address up to the '@' sign. 53 */ 54 private String simplifiedName; 55 56 // Regex that matches address surrounded by '<>' optionally. '^<?([^>]+)>?$' 57 private static final Pattern REMOVE_OPTIONAL_BRACKET = Pattern.compile("^<?([^>]+)>?$"); 58 // Regex that matches personal name surrounded by '""' optionally. '^"?([^"]+)"?$' 59 private static final Pattern REMOVE_OPTIONAL_DQUOTE = Pattern.compile("^\"?([^\"]*)\"?$"); 60 // Regex that matches escaped character '\\([\\"])' 61 private static final Pattern UNQUOTE = Pattern.compile("\\\\([\\\\\"])"); 62 63 // TODO: LOCAL_PART and DOMAIN_PART_PART are too permissive and can be improved. 64 // TODO: Fix this to better constrain comments. 65 /** Regex for the local part of an email address. */ 66 private static final String LOCAL_PART = "[^@]+"; 67 /** Regex for each part of the domain part, i.e. the thing between the dots. */ 68 private static final String DOMAIN_PART_PART = "[[\\w][\\d]\\-\\(\\)\\[\\]]+"; 69 /** Regex for the domain part, which is two or more {@link #DOMAIN_PART_PART} separated by . */ 70 private static final String DOMAIN_PART = "(" + DOMAIN_PART_PART + "\\.)+" + DOMAIN_PART_PART; 71 72 /** Pattern to check if an email address is valid. */ 73 private static final Pattern EMAIL_ADDRESS = 74 Pattern.compile("\\A" + LOCAL_PART + "@" + DOMAIN_PART + "\\z"); 75 76 private static final Address[] EMPTY_ADDRESS_ARRAY = new Address[0]; 77 78 // delimiters are chars that do not appear in an email address, used by fromHeader 79 private static final char LIST_DELIMITER_EMAIL = '\1'; 80 private static final char LIST_DELIMITER_PERSONAL = '\2'; 81 82 private static final String LOG_TAG = "Email Address"; 83 84 @VisibleForTesting Address(String address)85 public Address(String address) { 86 setAddress(address); 87 } 88 Address(String address, String personal)89 public Address(String address, String personal) { 90 setPersonal(personal); 91 setAddress(address); 92 } 93 94 /** 95 * Returns a simplified string for this e-mail address. When a name is known, it will return the 96 * first token of that name. Otherwise, it will return the e-mail address up to the '@' sign. 97 */ getSimplifiedName()98 public String getSimplifiedName() { 99 if (simplifiedName == null) { 100 if (TextUtils.isEmpty(personal) && !TextUtils.isEmpty(address)) { 101 int atSign = address.indexOf('@'); 102 simplifiedName = (atSign != -1) ? address.substring(0, atSign) : ""; 103 } else if (!TextUtils.isEmpty(personal)) { 104 105 // TODO: use Contacts' NameSplitter for more reliable first-name extraction 106 107 int end = personal.indexOf(' '); 108 while (end > 0 && personal.charAt(end - 1) == ',') { 109 end--; 110 } 111 simplifiedName = (end < 1) ? personal : personal.substring(0, end); 112 113 } else { 114 LogUtils.w(LOG_TAG, "Unable to get a simplified name"); 115 simplifiedName = ""; 116 } 117 } 118 return simplifiedName; 119 } 120 getEmailAddress(String rawAddress)121 public static synchronized Address getEmailAddress(String rawAddress) { 122 if (TextUtils.isEmpty(rawAddress)) { 123 return null; 124 } 125 String name; 126 String address; 127 final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(rawAddress); 128 if (tokens.length > 0) { 129 final String tokenizedName = tokens[0].getName(); 130 name = tokenizedName != null ? Html.fromHtml(tokenizedName.trim()).toString() : ""; 131 address = Html.fromHtml(tokens[0].getAddress()).toString(); 132 } else { 133 name = ""; 134 address = rawAddress == null ? "" : Html.fromHtml(rawAddress).toString(); 135 } 136 return new Address(address, name); 137 } 138 getAddress()139 public String getAddress() { 140 return address; 141 } 142 setAddress(String address)143 public void setAddress(String address) { 144 this.address = REMOVE_OPTIONAL_BRACKET.matcher(address).replaceAll("$1"); 145 } 146 147 /** 148 * Get name part as UTF-16 string. No surrounding double quote, and no MIME/base64 encoding. 149 * 150 * @return Name part of email address. Returns null if it is omitted. 151 */ getPersonal()152 public String getPersonal() { 153 return personal; 154 } 155 156 /** 157 * Set personal part from UTF-16 string. Optional surrounding double quote will be removed. It 158 * will be also unquoted and MIME/base64 decoded. 159 * 160 * @param personal name part of email address as UTF-16 string. Null is acceptable. 161 */ setPersonal(String personal)162 public void setPersonal(String personal) { 163 this.personal = decodeAddressPersonal(personal); 164 } 165 166 /** 167 * Decodes name from UTF-16 string. Optional surrounding double quote will be removed. It will be 168 * also unquoted and MIME/base64 decoded. 169 * 170 * @param personal name part of email address as UTF-16 string. Null is acceptable. 171 */ decodeAddressPersonal(String personal)172 public static String decodeAddressPersonal(String personal) { 173 if (personal != null) { 174 personal = REMOVE_OPTIONAL_DQUOTE.matcher(personal).replaceAll("$1"); 175 personal = UNQUOTE.matcher(personal).replaceAll("$1"); 176 personal = DecoderUtil.decodeEncodedWords(personal, DecodeMonitor.STRICT); 177 if (personal.length() == 0) { 178 personal = null; 179 } 180 } 181 return personal; 182 } 183 184 /** 185 * This method is used to check that all the addresses that the user entered in a list (e.g. To:) 186 * are valid, so that none is dropped. 187 */ 188 @VisibleForTesting isAllValid(String addressList)189 public static boolean isAllValid(String addressList) { 190 // This code mimics the parse() method below. 191 // I don't know how to better avoid the code-duplication. 192 if (addressList != null && addressList.length() > 0) { 193 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList); 194 for (int i = 0, length = tokens.length; i < length; ++i) { 195 Rfc822Token token = tokens[i]; 196 String address = token.getAddress(); 197 if (!TextUtils.isEmpty(address) && !isValidAddress(address)) { 198 return false; 199 } 200 } 201 } 202 return true; 203 } 204 205 /** 206 * Parse a comma-delimited list of addresses in RFC822 format and return an array of Address 207 * objects. 208 * 209 * @param addressList Address list in comma-delimited string. 210 * @return An array of 0 or more Addresses. 211 */ parse(String addressList)212 public static Address[] parse(String addressList) { 213 if (addressList == null || addressList.length() == 0) { 214 return EMPTY_ADDRESS_ARRAY; 215 } 216 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList); 217 ArrayList<Address> addresses = new ArrayList<Address>(); 218 for (int i = 0, length = tokens.length; i < length; ++i) { 219 Rfc822Token token = tokens[i]; 220 String address = token.getAddress(); 221 if (!TextUtils.isEmpty(address)) { 222 if (isValidAddress(address)) { 223 String name = token.getName(); 224 if (TextUtils.isEmpty(name)) { 225 name = null; 226 } 227 addresses.add(new Address(address, name)); 228 } 229 } 230 } 231 return addresses.toArray(new Address[addresses.size()]); 232 } 233 234 /** Checks whether a string email address is valid. E.g. name@domain.com is valid. */ 235 @VisibleForTesting isValidAddress(final String address)236 static boolean isValidAddress(final String address) { 237 return EMAIL_ADDRESS.matcher(address).find(); 238 } 239 240 @Override equals(Object o)241 public boolean equals(Object o) { 242 if (o instanceof Address) { 243 // It seems that the spec says that the "user" part is case-sensitive, 244 // while the domain part in case-insesitive. 245 // So foo@yahoo.com and Foo@yahoo.com are different. 246 // This may seem non-intuitive from the user POV, so we 247 // may re-consider it if it creates UI trouble. 248 // A problem case is "replyAll" sending to both 249 // a@b.c and to A@b.c, which turn out to be the same on the server. 250 // Leave unchanged for now (i.e. case-sensitive). 251 return getAddress().equals(((Address) o).getAddress()); 252 } 253 return super.equals(o); 254 } 255 256 @Override hashCode()257 public int hashCode() { 258 return getAddress().hashCode(); 259 } 260 261 /** 262 * Get human readable address string. Do not use this for email header. 263 * 264 * @return Human readable address string. Not quoted and not encoded. 265 */ 266 @Override toString()267 public String toString() { 268 if (personal != null && !personal.equals(address)) { 269 if (personal.matches(".*[\\(\\)<>@,;:\\\\\".\\[\\]].*")) { 270 return ensureQuotedString(personal) + " <" + address + ">"; 271 } else { 272 return personal + " <" + address + ">"; 273 } 274 } else { 275 return address; 276 } 277 } 278 279 /** 280 * Ensures that the given string starts and ends with the double quote character. The string is 281 * not modified in any way except to add the double quote character to start and end if it's not 282 * already there. 283 * 284 * <p>sample -> "sample" "sample" -> "sample" ""sample"" -> "sample" "sample"" -> "sample" 285 * sa"mp"le -> "sa"mp"le" "sa"mp"le" -> "sa"mp"le" (empty string) -> "" " -> "" 286 */ ensureQuotedString(String s)287 private static String ensureQuotedString(String s) { 288 if (s == null) { 289 return null; 290 } 291 if (!s.matches("^\".*\"$")) { 292 return "\"" + s + "\""; 293 } else { 294 return s; 295 } 296 } 297 298 /** 299 * Get human readable comma-delimited address string. 300 * 301 * @param addresses Address array 302 * @return Human readable comma-delimited address string. 303 */ 304 @VisibleForTesting toString(Address[] addresses)305 public static String toString(Address[] addresses) { 306 return toString(addresses, ADDRESS_DELIMETER); 307 } 308 309 /** 310 * Get human readable address strings joined with the specified separator. 311 * 312 * @param addresses Address array 313 * @param separator Separator 314 * @return Human readable comma-delimited address string. 315 */ toString(Address[] addresses, String separator)316 public static String toString(Address[] addresses, String separator) { 317 if (addresses == null || addresses.length == 0) { 318 return null; 319 } 320 if (addresses.length == 1) { 321 return addresses[0].toString(); 322 } 323 StringBuilder sb = new StringBuilder(addresses[0].toString()); 324 for (int i = 1; i < addresses.length; i++) { 325 sb.append(separator); 326 // TODO: investigate why this .trim() is needed. 327 sb.append(addresses[i].toString().trim()); 328 } 329 return sb.toString(); 330 } 331 332 /** 333 * Get RFC822/MIME compatible address string. 334 * 335 * @return RFC822/MIME compatible address string. It may be surrounded by double quote or quoted 336 * and MIME/base64 encoded if necessary. 337 */ toHeader()338 public String toHeader() { 339 if (personal != null) { 340 return EncoderUtil.encodeAddressDisplayName(personal) + " <" + address + ">"; 341 } else { 342 return address; 343 } 344 } 345 346 /** 347 * Get RFC822/MIME compatible comma-delimited address string. 348 * 349 * @param addresses Address array 350 * @return RFC822/MIME compatible comma-delimited address string. it may be surrounded by double 351 * quoted or quoted and MIME/base64 encoded if necessary. 352 */ toHeader(Address[] addresses)353 public static String toHeader(Address[] addresses) { 354 if (addresses == null || addresses.length == 0) { 355 return null; 356 } 357 if (addresses.length == 1) { 358 return addresses[0].toHeader(); 359 } 360 StringBuilder sb = new StringBuilder(addresses[0].toHeader()); 361 for (int i = 1; i < addresses.length; i++) { 362 // We need space character to be able to fold line. 363 sb.append(", "); 364 sb.append(addresses[i].toHeader()); 365 } 366 return sb.toString(); 367 } 368 369 /** 370 * Get Human friendly address string. 371 * 372 * @return the personal part of this Address, or the address part if the personal part is not 373 * available 374 */ 375 @VisibleForTesting toFriendly()376 public String toFriendly() { 377 if (personal != null && personal.length() > 0) { 378 return personal; 379 } else { 380 return address; 381 } 382 } 383 384 /** 385 * Creates a comma-delimited list of addresses in the "friendly" format (see toFriendly() for 386 * details on the per-address conversion). 387 * 388 * @param addresses Array of Address[] values 389 * @return A comma-delimited string listing all of the addresses supplied. Null if source was null 390 * or empty. 391 */ 392 @VisibleForTesting toFriendly(Address[] addresses)393 public static String toFriendly(Address[] addresses) { 394 if (addresses == null || addresses.length == 0) { 395 return null; 396 } 397 if (addresses.length == 1) { 398 return addresses[0].toFriendly(); 399 } 400 StringBuilder sb = new StringBuilder(addresses[0].toFriendly()); 401 for (int i = 1; i < addresses.length; i++) { 402 sb.append(", "); 403 sb.append(addresses[i].toFriendly()); 404 } 405 return sb.toString(); 406 } 407 408 /** Returns exactly the same result as Address.toString(Address.fromHeader(addressList)). */ 409 @VisibleForTesting fromHeaderToString(String addressList)410 public static String fromHeaderToString(String addressList) { 411 return toString(fromHeader(addressList)); 412 } 413 414 /** Returns exactly the same result as Address.toHeader(Address.parse(addressList)). */ 415 @VisibleForTesting parseToHeader(String addressList)416 public static String parseToHeader(String addressList) { 417 return Address.toHeader(Address.parse(addressList)); 418 } 419 420 /** 421 * Returns null if the addressList has 0 addresses, otherwise returns the first address. The same 422 * as Address.fromHeader(addressList)[0] for non-empty list. This is an utility method that offers 423 * some performance optimization opportunities. 424 */ 425 @VisibleForTesting firstAddress(String addressList)426 public static Address firstAddress(String addressList) { 427 Address[] array = fromHeader(addressList); 428 return array.length > 0 ? array[0] : null; 429 } 430 431 /** 432 * This method exists to convert an address list formatted in a deprecated legacy format to the 433 * standard RFC822 header format. {@link #fromHeader(String)} is capable of reading the legacy 434 * format and the RFC822 format. {@link #toHeader()} always produces the RFC822 format. 435 * 436 * <p>This implementation is brute-force, and could be replaced with a more efficient version if 437 * desired. 438 */ reformatToHeader(String addressList)439 public static String reformatToHeader(String addressList) { 440 return toHeader(fromHeader(addressList)); 441 } 442 443 /** 444 * @param addressList a CSV of RFC822 addresses or the deprecated legacy string format 445 * @return array of addresses parsed from <code>addressList</code> 446 */ 447 @VisibleForTesting fromHeader(String addressList)448 public static Address[] fromHeader(String addressList) { 449 if (addressList == null || addressList.length() == 0) { 450 return EMPTY_ADDRESS_ARRAY; 451 } 452 // IF we're CSV, just parse 453 if ((addressList.indexOf(LIST_DELIMITER_PERSONAL) == -1) 454 && (addressList.indexOf(LIST_DELIMITER_EMAIL) == -1)) { 455 return Address.parse(addressList); 456 } 457 // Otherwise, do backward-compatible unpack 458 ArrayList<Address> addresses = new ArrayList<Address>(); 459 int length = addressList.length(); 460 int pairStartIndex = 0; 461 int pairEndIndex; 462 463 /* addressEndIndex is only re-scanned (indexOf()) when a LIST_DELIMITER_PERSONAL 464 is used, not for every email address; i.e. not for every iteration of the while(). 465 This reduces the theoretical complexity from quadratic to linear, 466 and provides some speed-up in practice by removing redundant scans of the string. 467 */ 468 int addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL); 469 470 while (pairStartIndex < length) { 471 pairEndIndex = addressList.indexOf(LIST_DELIMITER_EMAIL, pairStartIndex); 472 if (pairEndIndex == -1) { 473 pairEndIndex = length; 474 } 475 Address address; 476 if (addressEndIndex == -1 || pairEndIndex <= addressEndIndex) { 477 // in this case the DELIMITER_PERSONAL is in a future pair, 478 // so don't use personal, and don't update addressEndIndex 479 address = new Address(addressList.substring(pairStartIndex, pairEndIndex), null); 480 } else { 481 address = 482 new Address( 483 addressList.substring(pairStartIndex, addressEndIndex), 484 addressList.substring(addressEndIndex + 1, pairEndIndex)); 485 // only update addressEndIndex when we use the LIST_DELIMITER_PERSONAL 486 addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL, pairEndIndex + 1); 487 } 488 addresses.add(address); 489 pairStartIndex = pairEndIndex + 1; 490 } 491 return addresses.toArray(new Address[addresses.size()]); 492 } 493 494 public static final Creator<Address> CREATOR = 495 new Creator<Address>() { 496 @Override 497 public Address createFromParcel(Parcel parcel) { 498 return new Address(parcel); 499 } 500 501 @Override 502 public Address[] newArray(int size) { 503 return new Address[size]; 504 } 505 }; 506 Address(Parcel in)507 public Address(Parcel in) { 508 setPersonal(in.readString()); 509 setAddress(in.readString()); 510 } 511 512 @Override describeContents()513 public int describeContents() { 514 return 0; 515 } 516 517 @Override writeToParcel(Parcel out, int flags)518 public void writeToParcel(Parcel out, int flags) { 519 out.writeString(personal); 520 out.writeString(address); 521 } 522 } 523