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