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