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