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