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.email.mail; 18 19 import com.android.email.Utility; 20 21 import org.apache.james.mime4j.codec.EncoderUtil; 22 import org.apache.james.mime4j.decoder.DecoderUtil; 23 24 import android.text.TextUtils; 25 import android.text.util.Rfc822Token; 26 import android.text.util.Rfc822Tokenizer; 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 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 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 Address(String address, String personal)67 public Address(String address, String personal) { 68 setAddress(address); 69 setPersonal(personal); 70 } 71 Address(String address)72 public Address(String address) { 73 setAddress(address); 74 } 75 getAddress()76 public String getAddress() { 77 return mAddress; 78 } 79 setAddress(String address)80 public void setAddress(String address) { 81 this.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 */ getPersonal()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 */ setPersonal(String personal)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 this.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 */ isAllValid(String addressList)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 */ parse(String addressList)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 */ isValidAddress(String address)164 /* package */ static boolean isValidAddress(String address) { 165 // Note: Some email provider may violate the standard, so here we only check that 166 // address consists of two part that are separated by '@', and domain part contains 167 // at least one '.'. 168 int len = address.length(); 169 int firstAt = address.indexOf('@'); 170 int lastAt = address.lastIndexOf('@'); 171 int firstDot = address.indexOf('.', lastAt + 1); 172 int lastDot = address.lastIndexOf('.'); 173 return firstAt > 0 && firstAt == lastAt && lastAt + 1 < firstDot 174 && firstDot <= lastDot && lastDot < len - 1; 175 } 176 177 @Override equals(Object o)178 public boolean equals(Object o) { 179 if (o instanceof Address) { 180 // It seems that the spec says that the "user" part is case-sensitive, 181 // while the domain part in case-insesitive. 182 // So foo@yahoo.com and Foo@yahoo.com are different. 183 // This may seem non-intuitive from the user POV, so we 184 // may re-consider it if it creates UI trouble. 185 // A problem case is "replyAll" sending to both 186 // a@b.c and to A@b.c, which turn out to be the same on the server. 187 // Leave unchanged for now (i.e. case-sensitive). 188 return getAddress().equals(((Address) o).getAddress()); 189 } 190 return super.equals(o); 191 } 192 193 /** 194 * Get human readable address string. 195 * Do not use this for email header. 196 * 197 * @return Human readable address string. Not quoted and not encoded. 198 */ toString()199 public String toString() { 200 if (mPersonal != null) { 201 if (mPersonal.matches(".*[\\(\\)<>@,;:\\\\\".\\[\\]].*")) { 202 return Utility.quoteString(mPersonal) + " <" + mAddress + ">"; 203 } else { 204 return mPersonal + " <" + mAddress + ">"; 205 } 206 } else { 207 return mAddress; 208 } 209 } 210 211 /** 212 * Get human readable comma-delimited address string. 213 * 214 * @param addresses Address array 215 * @return Human readable comma-delimited address string. 216 */ toString(Address[] addresses)217 public static String toString(Address[] addresses) { 218 if (addresses == null || addresses.length == 0) { 219 return null; 220 } 221 if (addresses.length == 1) { 222 return addresses[0].toString(); 223 } 224 StringBuffer sb = new StringBuffer(addresses[0].toString()); 225 for (int i = 1; i < addresses.length; i++) { 226 sb.append(','); 227 sb.append(addresses[i].toString()); 228 } 229 return sb.toString(); 230 } 231 232 /** 233 * Get RFC822/MIME compatible address string. 234 * 235 * @return RFC822/MIME compatible address string. 236 * It may be surrounded by double quote or quoted and MIME/base64 encoded if necessary. 237 */ toHeader()238 public String toHeader() { 239 if (mPersonal != null) { 240 return EncoderUtil.encodeAddressDisplayName(mPersonal) + " <" + mAddress + ">"; 241 } else { 242 return mAddress; 243 } 244 } 245 246 /** 247 * Get RFC822/MIME compatible comma-delimited address string. 248 * 249 * @param addresses Address array 250 * @return RFC822/MIME compatible comma-delimited address string. 251 * it may be surrounded by double quoted or quoted and MIME/base64 encoded if necessary. 252 */ toHeader(Address[] addresses)253 public static String toHeader(Address[] addresses) { 254 if (addresses == null || addresses.length == 0) { 255 return null; 256 } 257 if (addresses.length == 1) { 258 return addresses[0].toHeader(); 259 } 260 StringBuffer sb = new StringBuffer(addresses[0].toHeader()); 261 for (int i = 1; i < addresses.length; i++) { 262 // We need space character to be able to fold line. 263 sb.append(", "); 264 sb.append(addresses[i].toHeader()); 265 } 266 return sb.toString(); 267 } 268 269 /** 270 * Get Human friendly address string. 271 * 272 * @return the personal part of this Address, or the address part if the 273 * personal part is not available 274 */ toFriendly()275 public String toFriendly() { 276 if (mPersonal != null && mPersonal.length() > 0) { 277 return mPersonal; 278 } else { 279 return mAddress; 280 } 281 } 282 283 /** 284 * Creates a comma-delimited list of addresses in the "friendly" format (see toFriendly() for 285 * details on the per-address conversion). 286 * 287 * @param addresses Array of Address[] values 288 * @return A comma-delimited string listing all of the addresses supplied. Null if source 289 * was null or empty. 290 */ toFriendly(Address[] addresses)291 public static String toFriendly(Address[] addresses) { 292 if (addresses == null || addresses.length == 0) { 293 return null; 294 } 295 if (addresses.length == 1) { 296 return addresses[0].toFriendly(); 297 } 298 StringBuffer sb = new StringBuffer(addresses[0].toFriendly()); 299 for (int i = 1; i < addresses.length; i++) { 300 sb.append(','); 301 sb.append(addresses[i].toFriendly()); 302 } 303 return sb.toString(); 304 } 305 306 /** 307 * Returns exactly the same result as Address.toString(Address.unpack(packedList)). 308 */ unpackToString(String packedList)309 public static String unpackToString(String packedList) { 310 return toString(unpack(packedList)); 311 } 312 313 /** 314 * Returns exactly the same result as Address.pack(Address.parse(textList)). 315 */ parseAndPack(String textList)316 public static String parseAndPack(String textList) { 317 return Address.pack(Address.parse(textList)); 318 } 319 320 /** 321 * Returns null if the packedList has 0 addresses, otherwise returns the first address. 322 * The same as Address.unpack(packedList)[0] for non-empty list. 323 * This is an utility method that offers some performance optimization opportunities. 324 */ unpackFirst(String packedList)325 public static Address unpackFirst(String packedList) { 326 Address[] array = unpack(packedList); 327 return array.length > 0 ? array[0] : null; 328 } 329 330 /** 331 * Convert a packed list of addresses to a form suitable for use in an RFC822 header. 332 * This implementation is brute-force, and could be replaced with a more efficient version 333 * if desired. 334 */ packedToHeader(String packedList)335 public static String packedToHeader(String packedList) { 336 return toHeader(unpack(packedList)); 337 } 338 339 /** 340 * Unpacks an address list previously packed with pack() 341 * @param addressList String with packed addresses as returned by pack() 342 * @return array of addresses resulting from unpack 343 */ unpack(String addressList)344 public static Address[] unpack(String addressList) { 345 if (addressList == null || addressList.length() == 0) { 346 return EMPTY_ADDRESS_ARRAY; 347 } 348 ArrayList<Address> addresses = new ArrayList<Address>(); 349 int length = addressList.length(); 350 int pairStartIndex = 0; 351 int pairEndIndex = 0; 352 353 /* addressEndIndex is only re-scanned (indexOf()) when a LIST_DELIMITER_PERSONAL 354 is used, not for every email address; i.e. not for every iteration of the while(). 355 This reduces the theoretical complexity from quadratic to linear, 356 and provides some speed-up in practice by removing redundant scans of the string. 357 */ 358 int addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL); 359 360 while (pairStartIndex < length) { 361 pairEndIndex = addressList.indexOf(LIST_DELIMITER_EMAIL, pairStartIndex); 362 if (pairEndIndex == -1) { 363 pairEndIndex = length; 364 } 365 Address address; 366 if (addressEndIndex == -1 || pairEndIndex <= addressEndIndex) { 367 // in this case the DELIMITER_PERSONAL is in a future pair, 368 // so don't use personal, and don't update addressEndIndex 369 address = new Address(addressList.substring(pairStartIndex, pairEndIndex), null); 370 } else { 371 address = new Address(addressList.substring(pairStartIndex, addressEndIndex), 372 addressList.substring(addressEndIndex + 1, pairEndIndex)); 373 // only update addressEndIndex when we use the LIST_DELIMITER_PERSONAL 374 addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL, pairEndIndex + 1); 375 } 376 addresses.add(address); 377 pairStartIndex = pairEndIndex + 1; 378 } 379 return addresses.toArray(EMPTY_ADDRESS_ARRAY); 380 } 381 382 /** 383 * Packs an address list into a String that is very quick to read 384 * and parse. Packed lists can be unpacked with unpack(). 385 * The format is a series of packed addresses separated by LIST_DELIMITER_EMAIL. 386 * Each address is packed as 387 * a pair of address and personal separated by LIST_DELIMITER_PERSONAL, 388 * where the personal and delimiter are optional. 389 * E.g. "foo@x.com\1joe@x.com\2Joe Doe" 390 * @param addresses Array of addresses 391 * @return a string containing the packed addresses. 392 */ pack(Address[] addresses)393 public static String pack(Address[] addresses) { 394 // TODO: return same value for both null & empty list 395 if (addresses == null) { 396 return null; 397 } 398 final int nAddr = addresses.length; 399 if (nAddr == 0) { 400 return ""; 401 } 402 403 // shortcut: one email with no displayName 404 if (nAddr == 1 && addresses[0].getPersonal() == null) { 405 return addresses[0].getAddress(); 406 } 407 408 StringBuffer sb = new StringBuffer(); 409 for (int i = 0; i < nAddr; i++) { 410 if (i != 0) { 411 sb.append(LIST_DELIMITER_EMAIL); 412 } 413 final Address address = addresses[i]; 414 sb.append(address.getAddress()); 415 final String displayName = address.getPersonal(); 416 if (displayName != null) { 417 sb.append(LIST_DELIMITER_PERSONAL); 418 sb.append(displayName); 419 } 420 } 421 return sb.toString(); 422 } 423 424 /** 425 * Produces the same result as pack(array), but only packs one (this) address. 426 */ pack()427 public String pack() { 428 final String address = getAddress(); 429 final String personal = getPersonal(); 430 if (personal == null) { 431 return address; 432 } else { 433 return address + LIST_DELIMITER_PERSONAL + personal; 434 } 435 } 436 437 /** 438 * Legacy unpack() used for reading the old data (migration), 439 * as found in LocalStore (Donut; db version up to 24). 440 * @See unpack() 441 */ legacyUnpack(String addressList)442 /* package */ static Address[] legacyUnpack(String addressList) { 443 if (addressList == null || addressList.length() == 0) { 444 return new Address[] { }; 445 } 446 ArrayList<Address> addresses = new ArrayList<Address>(); 447 int length = addressList.length(); 448 int pairStartIndex = 0; 449 int pairEndIndex = 0; 450 int addressEndIndex = 0; 451 while (pairStartIndex < length) { 452 pairEndIndex = addressList.indexOf(',', pairStartIndex); 453 if (pairEndIndex == -1) { 454 pairEndIndex = length; 455 } 456 addressEndIndex = addressList.indexOf(';', pairStartIndex); 457 String address = null; 458 String personal = null; 459 if (addressEndIndex == -1 || addressEndIndex > pairEndIndex) { 460 address = 461 Utility.fastUrlDecode(addressList.substring(pairStartIndex, pairEndIndex)); 462 } 463 else { 464 address = 465 Utility.fastUrlDecode(addressList.substring(pairStartIndex, addressEndIndex)); 466 personal = 467 Utility.fastUrlDecode(addressList.substring(addressEndIndex + 1, pairEndIndex)); 468 } 469 addresses.add(new Address(address, personal)); 470 pairStartIndex = pairEndIndex + 1; 471 } 472 return addresses.toArray(new Address[] { }); 473 } 474 } 475