Address.java revision aa27bc0e1c3bb6be4609b00007637a9d3e960f5e
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 17package com.android.emailcommon.mail; 18 19import android.text.TextUtils; 20import android.text.util.Rfc822Token; 21import android.text.util.Rfc822Tokenizer; 22 23import com.google.common.annotations.VisibleForTesting; 24 25import org.apache.james.mime4j.codec.EncoderUtil; 26import org.apache.james.mime4j.decoder.DecoderUtil; 27 28import java.util.ArrayList; 29import java.util.regex.Pattern; 30 31/** 32 * This class represent email address. 33 * 34 * RFC822 email address may have following format. 35 * "name" <address> (comment) 36 * "name" <address> 37 * name <address> 38 * address 39 * Name and comment part should be MIME/base64 encoded in header if necessary. 40 * 41 */ 42public class Address { 43 /** 44 * Address part, in the form local_part@domain_part. No surrounding angle brackets. 45 */ 46 private String mAddress; 47 48 /** 49 * Name part. No surrounding double quote, and no MIME/base64 encoding. 50 * This must be null if Address has no name part. 51 */ 52 private String mPersonal; 53 54 // Regex that matches address surrounded by '<>' optionally. '^<?([^>]+)>?$' 55 private static final Pattern REMOVE_OPTIONAL_BRACKET = Pattern.compile("^<?([^>]+)>?$"); 56 // Regex that matches personal name surrounded by '""' optionally. '^"?([^"]+)"?$' 57 private static final Pattern REMOVE_OPTIONAL_DQUOTE = Pattern.compile("^\"?([^\"]*)\"?$"); 58 // Regex that matches escaped character '\\([\\"])' 59 private static final Pattern UNQUOTE = Pattern.compile("\\\\([\\\\\"])"); 60 61 private static final Address[] EMPTY_ADDRESS_ARRAY = new Address[0]; 62 63 // delimiters are chars that do not appear in an email address, used by pack/unpack 64 private static final char LIST_DELIMITER_EMAIL = '\1'; 65 private static final char LIST_DELIMITER_PERSONAL = '\2'; 66 67 public Address(String address, String personal) { 68 setAddress(address); 69 setPersonal(personal); 70 } 71 72 public Address(String address) { 73 setAddress(address); 74 } 75 76 public String getAddress() { 77 return mAddress; 78 } 79 80 public void setAddress(String address) { 81 mAddress = REMOVE_OPTIONAL_BRACKET.matcher(address).replaceAll("$1"); 82 } 83 84 /** 85 * Get name part as UTF-16 string. No surrounding double quote, and no MIME/base64 encoding. 86 * 87 * @return Name part of email address. Returns null if it is omitted. 88 */ 89 public String getPersonal() { 90 return mPersonal; 91 } 92 93 /** 94 * Set name part from UTF-16 string. Optional surrounding double quote will be removed. 95 * It will be also unquoted and MIME/base64 decoded. 96 * 97 * @param personal name part of email address as UTF-16 string. Null is acceptable. 98 */ 99 public void setPersonal(String personal) { 100 if (personal != null) { 101 personal = REMOVE_OPTIONAL_DQUOTE.matcher(personal).replaceAll("$1"); 102 personal = UNQUOTE.matcher(personal).replaceAll("$1"); 103 personal = DecoderUtil.decodeEncodedWords(personal); 104 if (personal.length() == 0) { 105 personal = null; 106 } 107 } 108 mPersonal = personal; 109 } 110 111 /** 112 * This method is used to check that all the addresses that the user 113 * entered in a list (e.g. To:) are valid, so that none is dropped. 114 */ 115 public static boolean isAllValid(String addressList) { 116 // This code mimics the parse() method below. 117 // I don't know how to better avoid the code-duplication. 118 if (addressList != null && addressList.length() > 0) { 119 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList); 120 for (int i = 0, length = tokens.length; i < length; ++i) { 121 Rfc822Token token = tokens[i]; 122 String address = token.getAddress(); 123 if (!TextUtils.isEmpty(address) && !isValidAddress(address)) { 124 return false; 125 } 126 } 127 } 128 return true; 129 } 130 131 /** 132 * Parse a comma-delimited list of addresses in RFC822 format and return an 133 * array of Address objects. 134 * 135 * @param addressList Address list in comma-delimited string. 136 * @return An array of 0 or more Addresses. 137 */ 138 public static Address[] parse(String addressList) { 139 if (addressList == null || addressList.length() == 0) { 140 return EMPTY_ADDRESS_ARRAY; 141 } 142 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList); 143 ArrayList<Address> addresses = new ArrayList<Address>(); 144 for (int i = 0, length = tokens.length; i < length; ++i) { 145 Rfc822Token token = tokens[i]; 146 String address = token.getAddress(); 147 if (!TextUtils.isEmpty(address)) { 148 if (isValidAddress(address)) { 149 String name = token.getName(); 150 if (TextUtils.isEmpty(name)) { 151 name = null; 152 } 153 addresses.add(new Address(address, name)); 154 } 155 } 156 } 157 return addresses.toArray(new Address[] {}); 158 } 159 160 /** 161 * Checks whether a string email address is valid. 162 * E.g. name@domain.com is valid. 163 */ 164 @VisibleForTesting 165 static boolean isValidAddress(String address) { 166 // Note: Some email provider may violate the standard, so here we only check that 167 // address consists of two part that are separated by '@', and domain part contains 168 // at least one '.'. 169 int len = address.length(); 170 int firstAt = address.indexOf('@'); 171 int lastAt = address.lastIndexOf('@'); 172 int firstDot = address.indexOf('.', lastAt + 1); 173 int lastDot = address.lastIndexOf('.'); 174 return firstAt > 0 && firstAt == lastAt && lastAt + 1 < firstDot 175 && firstDot <= lastDot && lastDot < len - 1; 176 } 177 178 @Override 179 public boolean equals(Object o) { 180 if (o instanceof Address) { 181 // It seems that the spec says that the "user" part is case-sensitive, 182 // while the domain part in case-insesitive. 183 // So foo@yahoo.com and Foo@yahoo.com are different. 184 // This may seem non-intuitive from the user POV, so we 185 // may re-consider it if it creates UI trouble. 186 // A problem case is "replyAll" sending to both 187 // a@b.c and to A@b.c, which turn out to be the same on the server. 188 // Leave unchanged for now (i.e. case-sensitive). 189 return getAddress().equals(((Address) o).getAddress()); 190 } 191 return super.equals(o); 192 } 193 194 @Override 195 public int hashCode() { 196 return getAddress().hashCode(); 197 } 198 199 /** 200 * Get human readable address string. 201 * Do not use this for email header. 202 * 203 * @return Human readable address string. Not quoted and not encoded. 204 */ 205 @Override 206 public String toString() { 207 if (mPersonal != null && !mPersonal.equals(mAddress)) { 208 if (mPersonal.matches(".*[\\(\\)<>@,;:\\\\\".\\[\\]].*")) { 209 return quoteString(mPersonal) + " <" + mAddress + ">"; 210 } else { 211 return mPersonal + " <" + mAddress + ">"; 212 } 213 } else { 214 return mAddress; 215 } 216 } 217 218 /** 219 * Ensures that the given string starts and ends with the double quote character. The string is 220 * not modified in any way except to add the double quote character to start and end if it's not 221 * already there. 222 * 223 * TODO: Rename this, because "quoteString()" can mean so many different things. 224 * 225 * sample -> "sample" 226 * "sample" -> "sample" 227 * ""sample"" -> "sample" 228 * "sample"" -> "sample" 229 * sa"mp"le -> "sa"mp"le" 230 * "sa"mp"le" -> "sa"mp"le" 231 * (empty string) -> "" 232 * " -> "" 233 */ 234 public static String quoteString(String s) { 235 if (s == null) { 236 return null; 237 } 238 if (!s.matches("^\".*\"$")) { 239 return "\"" + s + "\""; 240 } 241 else { 242 return s; 243 } 244 } 245 246 /** 247 * Get human readable comma-delimited address string. 248 * 249 * @param addresses Address array 250 * @return Human readable comma-delimited address string. 251 */ 252 public static String toString(Address[] addresses) { 253 return toString(addresses, ","); 254 } 255 256 /** 257 * Get human readable address strings joined with the specified separator. 258 * 259 * @param addresses Address array 260 * @param separator Separator 261 * @return Human readable comma-delimited address string. 262 */ 263 public static String toString(Address[] addresses, String separator) { 264 if (addresses == null || addresses.length == 0) { 265 return null; 266 } 267 if (addresses.length == 1) { 268 return addresses[0].toString(); 269 } 270 StringBuffer sb = new StringBuffer(addresses[0].toString()); 271 for (int i = 1; i < addresses.length; i++) { 272 sb.append(separator); 273 // TODO: investigate why this .trim() is needed. 274 sb.append(addresses[i].toString().trim()); 275 } 276 return sb.toString(); 277 } 278 279 /** 280 * Get RFC822/MIME compatible address string. 281 * 282 * @return RFC822/MIME compatible address string. 283 * It may be surrounded by double quote or quoted and MIME/base64 encoded if necessary. 284 */ 285 public String toHeader() { 286 if (mPersonal != null) { 287 return EncoderUtil.encodeAddressDisplayName(mPersonal) + " <" + mAddress + ">"; 288 } else { 289 return mAddress; 290 } 291 } 292 293 /** 294 * Get RFC822/MIME compatible comma-delimited address string. 295 * 296 * @param addresses Address array 297 * @return RFC822/MIME compatible comma-delimited address string. 298 * it may be surrounded by double quoted or quoted and MIME/base64 encoded if necessary. 299 */ 300 public static String toHeader(Address[] addresses) { 301 if (addresses == null || addresses.length == 0) { 302 return null; 303 } 304 if (addresses.length == 1) { 305 return addresses[0].toHeader(); 306 } 307 StringBuffer sb = new StringBuffer(addresses[0].toHeader()); 308 for (int i = 1; i < addresses.length; i++) { 309 // We need space character to be able to fold line. 310 sb.append(", "); 311 sb.append(addresses[i].toHeader()); 312 } 313 return sb.toString(); 314 } 315 316 /** 317 * Get Human friendly address string. 318 * 319 * @return the personal part of this Address, or the address part if the 320 * personal part is not available 321 */ 322 public String toFriendly() { 323 if (mPersonal != null && mPersonal.length() > 0) { 324 return mPersonal; 325 } else { 326 return mAddress; 327 } 328 } 329 330 /** 331 * Creates a comma-delimited list of addresses in the "friendly" format (see toFriendly() for 332 * details on the per-address conversion). 333 * 334 * @param addresses Array of Address[] values 335 * @return A comma-delimited string listing all of the addresses supplied. Null if source 336 * was null or empty. 337 */ 338 public static String toFriendly(Address[] addresses) { 339 if (addresses == null || addresses.length == 0) { 340 return null; 341 } 342 if (addresses.length == 1) { 343 return addresses[0].toFriendly(); 344 } 345 StringBuffer sb = new StringBuffer(addresses[0].toFriendly()); 346 for (int i = 1; i < addresses.length; i++) { 347 sb.append(", "); 348 sb.append(addresses[i].toFriendly()); 349 } 350 return sb.toString(); 351 } 352 353 /** 354 * Returns exactly the same result as Address.toString(Address.unpack(packedList)). 355 */ 356 public static String unpackToString(String packedList) { 357 return toString(unpack(packedList)); 358 } 359 360 /** 361 * Returns exactly the same result as Address.pack(Address.parse(textList)). 362 */ 363 public static String parseAndPack(String textList) { 364 return Address.pack(Address.parse(textList)); 365 } 366 367 /** 368 * Returns null if the packedList has 0 addresses, otherwise returns the first address. 369 * The same as Address.unpack(packedList)[0] for non-empty list. 370 * This is an utility method that offers some performance optimization opportunities. 371 */ 372 public static Address unpackFirst(String packedList) { 373 Address[] array = unpack(packedList); 374 return array.length > 0 ? array[0] : null; 375 } 376 377 /** 378 * Convert a packed list of addresses to a form suitable for use in an RFC822 header. 379 * This implementation is brute-force, and could be replaced with a more efficient version 380 * if desired. 381 */ 382 public static String packedToHeader(String packedList) { 383 return toHeader(unpack(packedList)); 384 } 385 386 /** 387 * Unpacks an address list that is either CSV of RFC822 addresses OR (for backward 388 * compatibility) previously packed with pack() 389 * @param addressList string packed with pack() or CSV of RFC822 addresses 390 * @return array of addresses resulting from unpack 391 */ 392 public static Address[] unpack(String addressList) { 393 if (addressList == null || addressList.length() == 0) { 394 return EMPTY_ADDRESS_ARRAY; 395 } 396 // IF we're CSV, just parse 397 if ((addressList.indexOf(LIST_DELIMITER_PERSONAL) == -1) && 398 (addressList.indexOf(LIST_DELIMITER_EMAIL) == -1)) { 399 return Address.parse(addressList); 400 } 401 // Otherwise, do backward-compatibile unpack 402 ArrayList<Address> addresses = new ArrayList<Address>(); 403 int length = addressList.length(); 404 int pairStartIndex = 0; 405 int pairEndIndex = 0; 406 407 /* addressEndIndex is only re-scanned (indexOf()) when a LIST_DELIMITER_PERSONAL 408 is used, not for every email address; i.e. not for every iteration of the while(). 409 This reduces the theoretical complexity from quadratic to linear, 410 and provides some speed-up in practice by removing redundant scans of the string. 411 */ 412 int addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL); 413 414 while (pairStartIndex < length) { 415 pairEndIndex = addressList.indexOf(LIST_DELIMITER_EMAIL, pairStartIndex); 416 if (pairEndIndex == -1) { 417 pairEndIndex = length; 418 } 419 Address address; 420 if (addressEndIndex == -1 || pairEndIndex <= addressEndIndex) { 421 // in this case the DELIMITER_PERSONAL is in a future pair, 422 // so don't use personal, and don't update addressEndIndex 423 address = new Address(addressList.substring(pairStartIndex, pairEndIndex), null); 424 } else { 425 address = new Address(addressList.substring(pairStartIndex, addressEndIndex), 426 addressList.substring(addressEndIndex + 1, pairEndIndex)); 427 // only update addressEndIndex when we use the LIST_DELIMITER_PERSONAL 428 addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL, pairEndIndex + 1); 429 } 430 addresses.add(address); 431 pairStartIndex = pairEndIndex + 1; 432 } 433 return addresses.toArray(EMPTY_ADDRESS_ARRAY); 434 } 435 436 /** 437 * Generate a String containing RFC822 addresses separated by commas 438 * NOTE: We used to "pack" these addresses in an app-specific format, but no longer do so 439 */ 440 public static String pack(Address[] addresses) { 441 return Address.toHeader(addresses); 442 } 443 444 /** 445 * Produces the same result as pack(array), but only packs one (this) address. 446 */ 447 public String pack() { 448 final String address = getAddress(); 449 final String personal = getPersonal(); 450 if (personal == null) { 451 return address; 452 } else { 453 return address + LIST_DELIMITER_PERSONAL + personal; 454 } 455 } 456} 457