Address.java revision 4d3e937bb3ca208c320e8a124c6f26079c4090d0
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 */ 16package com.android.emailcommon.mail; 17 18import android.os.Parcel; 19import android.os.Parcelable; 20import android.text.Html; 21import android.text.TextUtils; 22import android.text.util.Rfc822Token; 23import android.text.util.Rfc822Tokenizer; 24 25import com.android.mail.utils.LogTag; 26import com.android.mail.utils.LogUtils; 27import com.google.common.annotations.VisibleForTesting; 28 29import org.apache.james.mime4j.codec.EncoderUtil; 30import org.apache.james.mime4j.decoder.DecoderUtil; 31 32import java.util.ArrayList; 33import 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 */ 46public 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 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 public static boolean isAllValid(String addressList) { 200 // This code mimics the parse() method below. 201 // I don't know how to better avoid the code-duplication. 202 if (addressList != null && addressList.length() > 0) { 203 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList); 204 for (int i = 0, length = tokens.length; i < length; ++i) { 205 Rfc822Token token = tokens[i]; 206 String address = token.getAddress(); 207 if (!TextUtils.isEmpty(address) && !isValidAddress(address)) { 208 return false; 209 } 210 } 211 } 212 return true; 213 } 214 215 /** 216 * Parse a comma-delimited list of addresses in RFC822 format and return an 217 * array of Address objects. 218 * 219 * @param addressList Address list in comma-delimited string. 220 * @return An array of 0 or more Addresses. 221 */ 222 public static Address[] parse(String addressList) { 223 if (addressList == null || addressList.length() == 0) { 224 return EMPTY_ADDRESS_ARRAY; 225 } 226 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList); 227 ArrayList<Address> addresses = new ArrayList<Address>(); 228 for (int i = 0, length = tokens.length; i < length; ++i) { 229 Rfc822Token token = tokens[i]; 230 String address = token.getAddress(); 231 if (!TextUtils.isEmpty(address)) { 232 if (isValidAddress(address)) { 233 String name = token.getName(); 234 if (TextUtils.isEmpty(name)) { 235 name = null; 236 } 237 addresses.add(new Address(address, name)); 238 } 239 } 240 } 241 return addresses.toArray(new Address[addresses.size()]); 242 } 243 244 /** 245 * Checks whether a string email address is valid. 246 * E.g. name@domain.com is valid. 247 */ 248 @VisibleForTesting 249 static boolean isValidAddress(final String address) { 250 return EMAIL_ADDRESS.matcher(address).find(); 251 } 252 253 @Override 254 public boolean equals(Object o) { 255 if (o instanceof Address) { 256 // It seems that the spec says that the "user" part is case-sensitive, 257 // while the domain part in case-insesitive. 258 // So foo@yahoo.com and Foo@yahoo.com are different. 259 // This may seem non-intuitive from the user POV, so we 260 // may re-consider it if it creates UI trouble. 261 // A problem case is "replyAll" sending to both 262 // a@b.c and to A@b.c, which turn out to be the same on the server. 263 // Leave unchanged for now (i.e. case-sensitive). 264 return getAddress().equals(((Address) o).getAddress()); 265 } 266 return super.equals(o); 267 } 268 269 @Override 270 public int hashCode() { 271 return getAddress().hashCode(); 272 } 273 274 /** 275 * Get human readable address string. 276 * Do not use this for email header. 277 * 278 * @return Human readable address string. Not quoted and not encoded. 279 */ 280 @Override 281 public String toString() { 282 if (mPersonal != null && !mPersonal.equals(mAddress)) { 283 if (mPersonal.matches(".*[\\(\\)<>@,;:\\\\\".\\[\\]].*")) { 284 return ensureQuotedString(mPersonal) + " <" + mAddress + ">"; 285 } else { 286 return mPersonal + " <" + mAddress + ">"; 287 } 288 } else { 289 return mAddress; 290 } 291 } 292 293 /** 294 * Ensures that the given string starts and ends with the double quote character. The string is 295 * not modified in any way except to add the double quote character to start and end if it's not 296 * already there. 297 * 298 * sample -> "sample" 299 * "sample" -> "sample" 300 * ""sample"" -> "sample" 301 * "sample"" -> "sample" 302 * sa"mp"le -> "sa"mp"le" 303 * "sa"mp"le" -> "sa"mp"le" 304 * (empty string) -> "" 305 * " -> "" 306 */ 307 private static String ensureQuotedString(String s) { 308 if (s == null) { 309 return null; 310 } 311 if (!s.matches("^\".*\"$")) { 312 return "\"" + s + "\""; 313 } else { 314 return s; 315 } 316 } 317 318 /** 319 * Get human readable comma-delimited address string. 320 * 321 * @param addresses Address array 322 * @return Human readable comma-delimited address string. 323 */ 324 public static String toString(Address[] addresses) { 325 return toString(addresses, ADDRESS_DELIMETER); 326 } 327 328 /** 329 * Get human readable address strings joined with the specified separator. 330 * 331 * @param addresses Address array 332 * @param separator Separator 333 * @return Human readable comma-delimited address string. 334 */ 335 public static String toString(Address[] addresses, String separator) { 336 if (addresses == null || addresses.length == 0) { 337 return null; 338 } 339 if (addresses.length == 1) { 340 return addresses[0].toString(); 341 } 342 StringBuilder sb = new StringBuilder(addresses[0].toString()); 343 for (int i = 1; i < addresses.length; i++) { 344 sb.append(separator); 345 // TODO: investigate why this .trim() is needed. 346 sb.append(addresses[i].toString().trim()); 347 } 348 return sb.toString(); 349 } 350 351 /** 352 * Get RFC822/MIME compatible address string. 353 * 354 * @return RFC822/MIME compatible address string. 355 * It may be surrounded by double quote or quoted and MIME/base64 encoded if necessary. 356 */ 357 public String toHeader() { 358 if (mPersonal != null) { 359 return EncoderUtil.encodeAddressDisplayName(mPersonal) + " <" + mAddress + ">"; 360 } else { 361 return mAddress; 362 } 363 } 364 365 /** 366 * Get RFC822/MIME compatible comma-delimited address string. 367 * 368 * @param addresses Address array 369 * @return RFC822/MIME compatible comma-delimited address string. 370 * it may be surrounded by double quoted or quoted and MIME/base64 encoded if necessary. 371 */ 372 public static String toHeader(Address[] addresses) { 373 if (addresses == null || addresses.length == 0) { 374 return null; 375 } 376 if (addresses.length == 1) { 377 return addresses[0].toHeader(); 378 } 379 StringBuilder sb = new StringBuilder(addresses[0].toHeader()); 380 for (int i = 1; i < addresses.length; i++) { 381 // We need space character to be able to fold line. 382 sb.append(", "); 383 sb.append(addresses[i].toHeader()); 384 } 385 return sb.toString(); 386 } 387 388 /** 389 * Get Human friendly address string. 390 * 391 * @return the personal part of this Address, or the address part if the 392 * personal part is not available 393 */ 394 public String toFriendly() { 395 if (mPersonal != null && mPersonal.length() > 0) { 396 return mPersonal; 397 } else { 398 return mAddress; 399 } 400 } 401 402 /** 403 * Creates a comma-delimited list of addresses in the "friendly" format (see toFriendly() for 404 * details on the per-address conversion). 405 * 406 * @param addresses Array of Address[] values 407 * @return A comma-delimited string listing all of the addresses supplied. Null if source 408 * was null or empty. 409 */ 410 public static String toFriendly(Address[] addresses) { 411 if (addresses == null || addresses.length == 0) { 412 return null; 413 } 414 if (addresses.length == 1) { 415 return addresses[0].toFriendly(); 416 } 417 StringBuilder sb = new StringBuilder(addresses[0].toFriendly()); 418 for (int i = 1; i < addresses.length; i++) { 419 sb.append(", "); 420 sb.append(addresses[i].toFriendly()); 421 } 422 return sb.toString(); 423 } 424 425 /** 426 * Returns exactly the same result as Address.toString(Address.fromHeader(addressList)). 427 */ 428 public static String fromHeaderToString(String addressList) { 429 return toString(fromHeader(addressList)); 430 } 431 432 /** 433 * Returns exactly the same result as Address.toHeader(Address.parse(addressList)). 434 */ 435 public static String parseToHeader(String addressList) { 436 return Address.toHeader(Address.parse(addressList)); 437 } 438 439 /** 440 * Returns null if the addressList has 0 addresses, otherwise returns the first address. 441 * The same as Address.fromHeader(addressList)[0] for non-empty list. 442 * This is an utility method that offers some performance optimization opportunities. 443 */ 444 public static Address firstAddress(String addressList) { 445 Address[] array = fromHeader(addressList); 446 return array.length > 0 ? array[0] : null; 447 } 448 449 /** 450 * This method exists to convert an address list formatted in a deprecated legacy format to the 451 * standard RFC822 header format. {@link #fromHeader(String)} is capable of reading the legacy 452 * format and the RFC822 format. {@link #toHeader()} always produces the RFC822 format. 453 * 454 * This implementation is brute-force, and could be replaced with a more efficient version 455 * if desired. 456 */ 457 public static String reformatToHeader(String addressList) { 458 return toHeader(fromHeader(addressList)); 459 } 460 461 /** 462 * @param addressList a CSV of RFC822 addresses or the deprecated legacy string format 463 * @return array of addresses parsed from <code>addressList</code> 464 */ 465 public static Address[] fromHeader(String addressList) { 466 if (addressList == null || addressList.length() == 0) { 467 return EMPTY_ADDRESS_ARRAY; 468 } 469 // IF we're CSV, just parse 470 if ((addressList.indexOf(LIST_DELIMITER_PERSONAL) == -1) && 471 (addressList.indexOf(LIST_DELIMITER_EMAIL) == -1)) { 472 return Address.parse(addressList); 473 } 474 // Otherwise, do backward-compatible unpack 475 ArrayList<Address> addresses = new ArrayList<Address>(); 476 int length = addressList.length(); 477 int pairStartIndex = 0; 478 int pairEndIndex; 479 480 /* addressEndIndex is only re-scanned (indexOf()) when a LIST_DELIMITER_PERSONAL 481 is used, not for every email address; i.e. not for every iteration of the while(). 482 This reduces the theoretical complexity from quadratic to linear, 483 and provides some speed-up in practice by removing redundant scans of the string. 484 */ 485 int addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL); 486 487 while (pairStartIndex < length) { 488 pairEndIndex = addressList.indexOf(LIST_DELIMITER_EMAIL, pairStartIndex); 489 if (pairEndIndex == -1) { 490 pairEndIndex = length; 491 } 492 Address address; 493 if (addressEndIndex == -1 || pairEndIndex <= addressEndIndex) { 494 // in this case the DELIMITER_PERSONAL is in a future pair, 495 // so don't use personal, and don't update addressEndIndex 496 address = new Address(addressList.substring(pairStartIndex, pairEndIndex), null); 497 } else { 498 address = new Address(addressList.substring(pairStartIndex, addressEndIndex), 499 addressList.substring(addressEndIndex + 1, pairEndIndex)); 500 // only update addressEndIndex when we use the LIST_DELIMITER_PERSONAL 501 addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL, pairEndIndex + 1); 502 } 503 addresses.add(address); 504 pairStartIndex = pairEndIndex + 1; 505 } 506 return addresses.toArray(new Address[addresses.size()]); 507 } 508 509 public static final Creator<Address> CREATOR = new Creator<Address>() { 510 @Override 511 public Address createFromParcel(Parcel parcel) { 512 return new Address(parcel); 513 } 514 515 @Override 516 public Address[] newArray(int size) { 517 return new Address[size]; 518 } 519 }; 520 521 public Address(Parcel in) { 522 setPersonal(in.readString()); 523 setAddress(in.readString()); 524 } 525 526 @Override 527 public int describeContents() { 528 return 0; 529 } 530 531 @Override 532 public void writeToParcel(Parcel out, int flags) { 533 out.writeString(mPersonal); 534 out.writeString(mAddress); 535 } 536} 537