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