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.io.UnsupportedEncodingException; 29 import java.net.URLEncoder; 30 import java.util.ArrayList; 31 import java.util.regex.Pattern; 32 33 /** 34 * This class represent email address. 35 * 36 * RFC822 email address may have following format. 37 * "name" <address> (comment) 38 * "name" <address> 39 * name <address> 40 * address 41 * Name and comment part should be MIME/base64 encoded in header if necessary. 42 * 43 */ 44 public class Address { 45 /** 46 * Address part, in the form local_part@domain_part. No surrounding angle brackets. 47 */ 48 private String mAddress; 49 50 /** 51 * Name part. No surrounding double quote, and no MIME/base64 encoding. 52 * This must be null if Address has no name part. 53 */ 54 private String mPersonal; 55 56 // Regex that matches address surrounded by '<>' optionally. '^<?([^>]+)>?$' 57 private static final Pattern REMOVE_OPTIONAL_BRACKET = Pattern.compile("^<?([^>]+)>?$"); 58 // Regex that matches personal name surrounded by '""' optionally. '^"?([^"]+)"?$' 59 private static final Pattern REMOVE_OPTIONAL_DQUOTE = Pattern.compile("^\"?([^\"]*)\"?$"); 60 // Regex that matches escaped character '\\([\\"])' 61 private static final Pattern UNQUOTE = Pattern.compile("\\\\([\\\\\"])"); 62 63 private static final Address[] EMPTY_ADDRESS_ARRAY = new Address[0]; 64 65 // delimiters are chars that do not appear in an email address, used by pack/unpack 66 private static final char LIST_DELIMITER_EMAIL = '\1'; 67 private static final char LIST_DELIMITER_PERSONAL = '\2'; 68 69 public Address(String address, String personal) { 70 setAddress(address); 71 setPersonal(personal); 72 } 73 74 public Address(String address) { 75 setAddress(address); 76 } 77 78 public String getAddress() { 79 return mAddress; 80 } 81 82 public void setAddress(String address) { 83 this.mAddress = REMOVE_OPTIONAL_BRACKET.matcher(address).replaceAll("$1");; 84 } 85 86 /** 87 * Get name part as UTF-16 string. No surrounding double quote, and no MIME/base64 encoding. 88 * 89 * @return Name part of email address. Returns null if it is omitted. 90 */ 91 public String getPersonal() { 92 return mPersonal; 93 } 94 95 /** 96 * Set name part from UTF-16 string. Optional surrounding double quote will be removed. 97 * It will be also unquoted and MIME/base64 decoded. 98 * 99 * @param Personal name part of email address as UTF-16 string. Null is acceptable. 100 */ 101 public void setPersonal(String personal) { 102 if (personal != null) { 103 personal = REMOVE_OPTIONAL_DQUOTE.matcher(personal).replaceAll("$1"); 104 personal = UNQUOTE.matcher(personal).replaceAll("$1"); 105 personal = DecoderUtil.decodeEncodedWords(personal); 106 if (personal.length() == 0) { 107 personal = null; 108 } 109 } 110 this.mPersonal = personal; 111 } 112 113 /** 114 * This method is used to check that all the addresses that the user 115 * entered in a list (e.g. To:) are valid, so that none is dropped. 116 */ 117 public static boolean isAllValid(String addressList) { 118 // This code mimics the parse() method below. 119 // I don't know how to better avoid the code-duplication. 120 if (addressList != null && addressList.length() > 0) { 121 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList); 122 for (int i = 0, length = tokens.length; i < length; ++i) { 123 Rfc822Token token = tokens[i]; 124 String address = token.getAddress(); 125 if (!TextUtils.isEmpty(address) && !isValidAddress(address)) { 126 return false; 127 } 128 } 129 } 130 return true; 131 } 132 133 /** 134 * Parse a comma-delimited list of addresses in RFC822 format and return an 135 * array of Address objects. 136 * 137 * @param addressList Address list in comma-delimited string. 138 * @return An array of 0 or more Addresses. 139 */ 140 public static Address[] parse(String addressList) { 141 if (addressList == null || addressList.length() == 0) { 142 return EMPTY_ADDRESS_ARRAY; 143 } 144 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList); 145 ArrayList<Address> addresses = new ArrayList<Address>(); 146 for (int i = 0, length = tokens.length; i < length; ++i) { 147 Rfc822Token token = tokens[i]; 148 String address = token.getAddress(); 149 if (!TextUtils.isEmpty(address)) { 150 if (isValidAddress(address)) { 151 String name = token.getName(); 152 if (TextUtils.isEmpty(name)) { 153 name = null; 154 } 155 addresses.add(new Address(address, name)); 156 } 157 } 158 } 159 return addresses.toArray(new Address[] {}); 160 } 161 162 /** 163 * Checks whether a string email address is valid. 164 * E.g. name (at) domain.com is valid. 165 */ 166 /* package */ 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) { 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 if (addresses == null || addresses.length == 0) { 222 return null; 223 } 224 if (addresses.length == 1) { 225 return addresses[0].toString(); 226 } 227 StringBuffer sb = new StringBuffer(addresses[0].toString()); 228 for (int i = 1; i < addresses.length; i++) { 229 sb.append(','); 230 sb.append(addresses[i].toString()); 231 } 232 return sb.toString(); 233 } 234 235 /** 236 * Get RFC822/MIME compatible address string. 237 * 238 * @return RFC822/MIME compatible address string. 239 * It may be surrounded by double quote or quoted and MIME/base64 encoded if necessary. 240 */ 241 public String toHeader() { 242 if (mPersonal != null) { 243 return EncoderUtil.encodeAddressDisplayName(mPersonal) + " <" + mAddress + ">"; 244 } else { 245 return mAddress; 246 } 247 } 248 249 /** 250 * Get RFC822/MIME compatible comma-delimited address string. 251 * 252 * @param addresses Address array 253 * @return RFC822/MIME compatible comma-delimited address string. 254 * it may be surrounded by double quoted or quoted and MIME/base64 encoded if necessary. 255 */ 256 public static String toHeader(Address[] addresses) { 257 if (addresses == null || addresses.length == 0) { 258 return null; 259 } 260 if (addresses.length == 1) { 261 return addresses[0].toHeader(); 262 } 263 StringBuffer sb = new StringBuffer(addresses[0].toHeader()); 264 for (int i = 1; i < addresses.length; i++) { 265 // We need space character to be able to fold line. 266 sb.append(", "); 267 sb.append(addresses[i].toHeader()); 268 } 269 return sb.toString(); 270 } 271 272 /** 273 * Get Human friendly address string. 274 * 275 * @return the personal part of this Address, or the address part if the 276 * personal part is not available 277 */ 278 public String toFriendly() { 279 if (mPersonal != null && mPersonal.length() > 0) { 280 return mPersonal; 281 } else { 282 return mAddress; 283 } 284 } 285 286 /** 287 * Creates a comma-delimited list of addresses in the "friendly" format (see toFriendly() for 288 * details on the per-address conversion). 289 * 290 * @param addresses Array of Address[] values 291 * @return A comma-delimited string listing all of the addresses supplied. Null if source 292 * was null or empty. 293 */ 294 public static String toFriendly(Address[] addresses) { 295 if (addresses == null || addresses.length == 0) { 296 return null; 297 } 298 if (addresses.length == 1) { 299 return addresses[0].toFriendly(); 300 } 301 StringBuffer sb = new StringBuffer(addresses[0].toFriendly()); 302 for (int i = 1; i < addresses.length; i++) { 303 sb.append(','); 304 sb.append(addresses[i].toFriendly()); 305 } 306 return sb.toString(); 307 } 308 309 /** 310 * Returns exactly the same result as Address.toString(Address.unpack(packedList)). 311 */ 312 public static String unpackToString(String packedList) { 313 return toString(unpack(packedList)); 314 } 315 316 /** 317 * Returns exactly the same result as Address.pack(Address.parse(textList)). 318 */ 319 public static String parseAndPack(String textList) { 320 return Address.pack(Address.parse(textList)); 321 } 322 323 /** 324 * Returns null if the packedList has 0 addresses, otherwise returns the first address. 325 * The same as Address.unpack(packedList)[0] for non-empty list. 326 * This is an utility method that offers some performance optimization opportunities. 327 */ 328 public static Address unpackFirst(String packedList) { 329 Address[] array = unpack(packedList); 330 return array.length > 0 ? array[0] : null; 331 } 332 333 /** 334 * Convert a packed list of addresses to a form suitable for use in an RFC822 header. 335 * This implementation is brute-force, and could be replaced with a more efficient version 336 * if desired. 337 */ 338 public static String packedToHeader(String packedList) { 339 return toHeader(unpack(packedList)); 340 } 341 342 /** 343 * Unpacks an address list previously packed with pack() 344 * @param addressList String with packed addresses as returned by pack() 345 * @return array of addresses resulting from unpack 346 */ 347 public static Address[] unpack(String addressList) { 348 if (addressList == null || addressList.length() == 0) { 349 return EMPTY_ADDRESS_ARRAY; 350 } 351 ArrayList<Address> addresses = new ArrayList<Address>(); 352 int length = addressList.length(); 353 int pairStartIndex = 0; 354 int pairEndIndex = 0; 355 356 /* addressEndIndex is only re-scanned (indexOf()) when a LIST_DELIMITER_PERSONAL 357 is used, not for every email address; i.e. not for every iteration of the while(). 358 This reduces the theoretical complexity from quadratic to linear, 359 and provides some speed-up in practice by removing redundant scans of the string. 360 */ 361 int addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL); 362 363 while (pairStartIndex < length) { 364 pairEndIndex = addressList.indexOf(LIST_DELIMITER_EMAIL, pairStartIndex); 365 if (pairEndIndex == -1) { 366 pairEndIndex = length; 367 } 368 Address address; 369 if (addressEndIndex == -1 || pairEndIndex <= addressEndIndex) { 370 // in this case the DELIMITER_PERSONAL is in a future pair, 371 // so don't use personal, and don't update addressEndIndex 372 address = new Address(addressList.substring(pairStartIndex, pairEndIndex), null); 373 } else { 374 address = new Address(addressList.substring(pairStartIndex, addressEndIndex), 375 addressList.substring(addressEndIndex + 1, pairEndIndex)); 376 // only update addressEndIndex when we use the LIST_DELIMITER_PERSONAL 377 addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL, pairEndIndex + 1); 378 } 379 addresses.add(address); 380 pairStartIndex = pairEndIndex + 1; 381 } 382 return addresses.toArray(EMPTY_ADDRESS_ARRAY); 383 } 384 385 /** 386 * Packs an address list into a String that is very quick to read 387 * and parse. Packed lists can be unpacked with unpack(). 388 * The format is a series of packed addresses separated by LIST_DELIMITER_EMAIL. 389 * Each address is packed as 390 * a pair of address and personal separated by LIST_DELIMITER_PERSONAL, 391 * where the personal and delimiter are optional. 392 * E.g. "foo (at) x.com\1joe (at) x.com\2Joe Doe" 393 * @param addresses Array of addresses 394 * @return a string containing the packed addresses. 395 */ 396 public static String pack(Address[] addresses) { 397 // TODO: return same value for both null & empty list 398 if (addresses == null) { 399 return null; 400 } 401 final int nAddr = addresses.length; 402 if (nAddr == 0) { 403 return ""; 404 } 405 406 // shortcut: one email with no displayName 407 if (nAddr == 1 && addresses[0].getPersonal() == null) { 408 return addresses[0].getAddress(); 409 } 410 411 StringBuffer sb = new StringBuffer(); 412 for (int i = 0; i < nAddr; i++) { 413 if (i != 0) { 414 sb.append(LIST_DELIMITER_EMAIL); 415 } 416 final Address address = addresses[i]; 417 sb.append(address.getAddress()); 418 final String displayName = address.getPersonal(); 419 if (displayName != null) { 420 sb.append(LIST_DELIMITER_PERSONAL); 421 sb.append(displayName); 422 } 423 } 424 return sb.toString(); 425 } 426 427 /** 428 * Produces the same result as pack(array), but only packs one (this) address. 429 */ 430 public String pack() { 431 final String address = getAddress(); 432 final String personal = getPersonal(); 433 if (personal == null) { 434 return address; 435 } else { 436 return address + LIST_DELIMITER_PERSONAL + personal; 437 } 438 } 439 440 /** 441 * Legacy unpack() used for reading the old data (migration), 442 * as found in LocalStore (Donut; db version up to 24). 443 * @See unpack() 444 */ 445 public static Address[] legacyUnpack(String addressList) { 446 if (addressList == null || addressList.length() == 0) { 447 return new Address[] { }; 448 } 449 ArrayList<Address> addresses = new ArrayList<Address>(); 450 int length = addressList.length(); 451 int pairStartIndex = 0; 452 int pairEndIndex = 0; 453 int addressEndIndex = 0; 454 while (pairStartIndex < length) { 455 pairEndIndex = addressList.indexOf(',', pairStartIndex); 456 if (pairEndIndex == -1) { 457 pairEndIndex = length; 458 } 459 addressEndIndex = addressList.indexOf(';', pairStartIndex); 460 String address = null; 461 String personal = null; 462 if (addressEndIndex == -1 || addressEndIndex > pairEndIndex) { 463 address = 464 Utility.fastUrlDecode(addressList.substring(pairStartIndex, pairEndIndex)); 465 } 466 else { 467 address = 468 Utility.fastUrlDecode(addressList.substring(pairStartIndex, addressEndIndex)); 469 personal = 470 Utility.fastUrlDecode(addressList.substring(addressEndIndex + 1, pairEndIndex)); 471 } 472 addresses.add(new Address(address, personal)); 473 pairStartIndex = pairEndIndex + 1; 474 } 475 return addresses.toArray(new Address[] { }); 476 } 477 478 /** 479 * Legacy pack() used for writing to old data (migration), 480 * as found in LocalStore (Donut; db version up to 24). 481 * @See unpack() 482 */ 483 public static String legacyPack(Address[] addresses) { 484 if (addresses == null) { 485 return null; 486 } else if (addresses.length == 0) { 487 return ""; 488 } 489 StringBuffer sb = new StringBuffer(); 490 for (int i = 0, count = addresses.length; i < count; i++) { 491 Address address = addresses[i]; 492 try { 493 sb.append(URLEncoder.encode(address.getAddress(), "UTF-8")); 494 if (address.getPersonal() != null) { 495 sb.append(';'); 496 sb.append(URLEncoder.encode(address.getPersonal(), "UTF-8")); 497 } 498 if (i < count - 1) { 499 sb.append(','); 500 } 501 } 502 catch (UnsupportedEncodingException uee) { 503 return null; 504 } 505 } 506 return sb.toString(); 507 } 508 } 509