509 lines
18 KiB
Java
509 lines
18 KiB
Java
/*
|
|
* Copyright (C) 2008 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package com.android.email.mail;
|
|
|
|
import com.android.email.Utility;
|
|
|
|
import org.apache.james.mime4j.codec.EncoderUtil;
|
|
import org.apache.james.mime4j.decoder.DecoderUtil;
|
|
|
|
import android.text.TextUtils;
|
|
import android.text.util.Rfc822Token;
|
|
import android.text.util.Rfc822Tokenizer;
|
|
|
|
import java.io.UnsupportedEncodingException;
|
|
import java.net.URLEncoder;
|
|
import java.util.ArrayList;
|
|
import java.util.regex.Pattern;
|
|
|
|
/**
|
|
* This class represent email address.
|
|
*
|
|
* RFC822 email address may have following format.
|
|
* "name" <address> (comment)
|
|
* "name" <address>
|
|
* name <address>
|
|
* address
|
|
* Name and comment part should be MIME/base64 encoded in header if necessary.
|
|
*
|
|
*/
|
|
public class Address {
|
|
/**
|
|
* Address part, in the form local_part@domain_part. No surrounding angle brackets.
|
|
*/
|
|
private String mAddress;
|
|
|
|
/**
|
|
* Name part. No surrounding double quote, and no MIME/base64 encoding.
|
|
* This must be null if Address has no name part.
|
|
*/
|
|
private String mPersonal;
|
|
|
|
// Regex that matches address surrounded by '<>' optionally. '^<?([^>]+)>?$'
|
|
private static final Pattern REMOVE_OPTIONAL_BRACKET = Pattern.compile("^<?([^>]+)>?$");
|
|
// Regex that matches personal name surrounded by '""' optionally. '^"?([^"]+)"?$'
|
|
private static final Pattern REMOVE_OPTIONAL_DQUOTE = Pattern.compile("^\"?([^\"]*)\"?$");
|
|
// Regex that matches escaped character '\\([\\"])'
|
|
private static final Pattern UNQUOTE = Pattern.compile("\\\\([\\\\\"])");
|
|
|
|
private static final Address[] EMPTY_ADDRESS_ARRAY = new Address[0];
|
|
|
|
// delimiters are chars that do not appear in an email address, used by pack/unpack
|
|
private static final char LIST_DELIMITER_EMAIL = '\1';
|
|
private static final char LIST_DELIMITER_PERSONAL = '\2';
|
|
|
|
public Address(String address, String personal) {
|
|
setAddress(address);
|
|
setPersonal(personal);
|
|
}
|
|
|
|
public Address(String address) {
|
|
setAddress(address);
|
|
}
|
|
|
|
public String getAddress() {
|
|
return mAddress;
|
|
}
|
|
|
|
public void setAddress(String address) {
|
|
this.mAddress = REMOVE_OPTIONAL_BRACKET.matcher(address).replaceAll("$1");;
|
|
}
|
|
|
|
/**
|
|
* Get name part as UTF-16 string. No surrounding double quote, and no MIME/base64 encoding.
|
|
*
|
|
* @return Name part of email address. Returns null if it is omitted.
|
|
*/
|
|
public String getPersonal() {
|
|
return mPersonal;
|
|
}
|
|
|
|
/**
|
|
* Set name part from UTF-16 string. Optional surrounding double quote will be removed.
|
|
* It will be also unquoted and MIME/base64 decoded.
|
|
*
|
|
* @param Personal name part of email address as UTF-16 string. Null is acceptable.
|
|
*/
|
|
public void setPersonal(String personal) {
|
|
if (personal != null) {
|
|
personal = REMOVE_OPTIONAL_DQUOTE.matcher(personal).replaceAll("$1");
|
|
personal = UNQUOTE.matcher(personal).replaceAll("$1");
|
|
personal = DecoderUtil.decodeEncodedWords(personal);
|
|
if (personal.length() == 0) {
|
|
personal = null;
|
|
}
|
|
}
|
|
this.mPersonal = personal;
|
|
}
|
|
|
|
/**
|
|
* This method is used to check that all the addresses that the user
|
|
* entered in a list (e.g. To:) are valid, so that none is dropped.
|
|
*/
|
|
public static boolean isAllValid(String addressList) {
|
|
// This code mimics the parse() method below.
|
|
// I don't know how to better avoid the code-duplication.
|
|
if (addressList != null && addressList.length() > 0) {
|
|
Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
|
|
for (int i = 0, length = tokens.length; i < length; ++i) {
|
|
Rfc822Token token = tokens[i];
|
|
String address = token.getAddress();
|
|
if (!TextUtils.isEmpty(address) && !isValidAddress(address)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Parse a comma-delimited list of addresses in RFC822 format and return an
|
|
* array of Address objects.
|
|
*
|
|
* @param addressList Address list in comma-delimited string.
|
|
* @return An array of 0 or more Addresses.
|
|
*/
|
|
public static Address[] parse(String addressList) {
|
|
if (addressList == null || addressList.length() == 0) {
|
|
return EMPTY_ADDRESS_ARRAY;
|
|
}
|
|
Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
|
|
ArrayList<Address> addresses = new ArrayList<Address>();
|
|
for (int i = 0, length = tokens.length; i < length; ++i) {
|
|
Rfc822Token token = tokens[i];
|
|
String address = token.getAddress();
|
|
if (!TextUtils.isEmpty(address)) {
|
|
if (isValidAddress(address)) {
|
|
String name = token.getName();
|
|
if (TextUtils.isEmpty(name)) {
|
|
name = null;
|
|
}
|
|
addresses.add(new Address(address, name));
|
|
}
|
|
}
|
|
}
|
|
return addresses.toArray(new Address[] {});
|
|
}
|
|
|
|
/**
|
|
* Checks whether a string email address is valid.
|
|
* E.g. name@domain.com is valid.
|
|
*/
|
|
/* package */ static boolean isValidAddress(String address) {
|
|
// Note: Some email provider may violate the standard, so here we only check that
|
|
// address consists of two part that are separated by '@', and domain part contains
|
|
// at least one '.'.
|
|
int len = address.length();
|
|
int firstAt = address.indexOf('@');
|
|
int lastAt = address.lastIndexOf('@');
|
|
int firstDot = address.indexOf('.', lastAt + 1);
|
|
int lastDot = address.lastIndexOf('.');
|
|
return firstAt > 0 && firstAt == lastAt && lastAt + 1 < firstDot
|
|
&& firstDot <= lastDot && lastDot < len - 1;
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object o) {
|
|
if (o instanceof Address) {
|
|
// It seems that the spec says that the "user" part is case-sensitive,
|
|
// while the domain part in case-insesitive.
|
|
// So foo@yahoo.com and Foo@yahoo.com are different.
|
|
// This may seem non-intuitive from the user POV, so we
|
|
// may re-consider it if it creates UI trouble.
|
|
// A problem case is "replyAll" sending to both
|
|
// a@b.c and to A@b.c, which turn out to be the same on the server.
|
|
// Leave unchanged for now (i.e. case-sensitive).
|
|
return getAddress().equals(((Address) o).getAddress());
|
|
}
|
|
return super.equals(o);
|
|
}
|
|
|
|
/**
|
|
* Get human readable address string.
|
|
* Do not use this for email header.
|
|
*
|
|
* @return Human readable address string. Not quoted and not encoded.
|
|
*/
|
|
@Override
|
|
public String toString() {
|
|
if (mPersonal != null && !mPersonal.equals(mAddress)) {
|
|
if (mPersonal.matches(".*[\\(\\)<>@,;:\\\\\".\\[\\]].*")) {
|
|
return Utility.quoteString(mPersonal) + " <" + mAddress + ">";
|
|
} else {
|
|
return mPersonal + " <" + mAddress + ">";
|
|
}
|
|
} else {
|
|
return mAddress;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get human readable comma-delimited address string.
|
|
*
|
|
* @param addresses Address array
|
|
* @return Human readable comma-delimited address string.
|
|
*/
|
|
public static String toString(Address[] addresses) {
|
|
if (addresses == null || addresses.length == 0) {
|
|
return null;
|
|
}
|
|
if (addresses.length == 1) {
|
|
return addresses[0].toString();
|
|
}
|
|
StringBuffer sb = new StringBuffer(addresses[0].toString());
|
|
for (int i = 1; i < addresses.length; i++) {
|
|
sb.append(',');
|
|
sb.append(addresses[i].toString());
|
|
}
|
|
return sb.toString();
|
|
}
|
|
|
|
/**
|
|
* Get RFC822/MIME compatible address string.
|
|
*
|
|
* @return RFC822/MIME compatible address string.
|
|
* It may be surrounded by double quote or quoted and MIME/base64 encoded if necessary.
|
|
*/
|
|
public String toHeader() {
|
|
if (mPersonal != null) {
|
|
return EncoderUtil.encodeAddressDisplayName(mPersonal) + " <" + mAddress + ">";
|
|
} else {
|
|
return mAddress;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get RFC822/MIME compatible comma-delimited address string.
|
|
*
|
|
* @param addresses Address array
|
|
* @return RFC822/MIME compatible comma-delimited address string.
|
|
* it may be surrounded by double quoted or quoted and MIME/base64 encoded if necessary.
|
|
*/
|
|
public static String toHeader(Address[] addresses) {
|
|
if (addresses == null || addresses.length == 0) {
|
|
return null;
|
|
}
|
|
if (addresses.length == 1) {
|
|
return addresses[0].toHeader();
|
|
}
|
|
StringBuffer sb = new StringBuffer(addresses[0].toHeader());
|
|
for (int i = 1; i < addresses.length; i++) {
|
|
// We need space character to be able to fold line.
|
|
sb.append(", ");
|
|
sb.append(addresses[i].toHeader());
|
|
}
|
|
return sb.toString();
|
|
}
|
|
|
|
/**
|
|
* Get Human friendly address string.
|
|
*
|
|
* @return the personal part of this Address, or the address part if the
|
|
* personal part is not available
|
|
*/
|
|
public String toFriendly() {
|
|
if (mPersonal != null && mPersonal.length() > 0) {
|
|
return mPersonal;
|
|
} else {
|
|
return mAddress;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a comma-delimited list of addresses in the "friendly" format (see toFriendly() for
|
|
* details on the per-address conversion).
|
|
*
|
|
* @param addresses Array of Address[] values
|
|
* @return A comma-delimited string listing all of the addresses supplied. Null if source
|
|
* was null or empty.
|
|
*/
|
|
public static String toFriendly(Address[] addresses) {
|
|
if (addresses == null || addresses.length == 0) {
|
|
return null;
|
|
}
|
|
if (addresses.length == 1) {
|
|
return addresses[0].toFriendly();
|
|
}
|
|
StringBuffer sb = new StringBuffer(addresses[0].toFriendly());
|
|
for (int i = 1; i < addresses.length; i++) {
|
|
sb.append(',');
|
|
sb.append(addresses[i].toFriendly());
|
|
}
|
|
return sb.toString();
|
|
}
|
|
|
|
/**
|
|
* Returns exactly the same result as Address.toString(Address.unpack(packedList)).
|
|
*/
|
|
public static String unpackToString(String packedList) {
|
|
return toString(unpack(packedList));
|
|
}
|
|
|
|
/**
|
|
* Returns exactly the same result as Address.pack(Address.parse(textList)).
|
|
*/
|
|
public static String parseAndPack(String textList) {
|
|
return Address.pack(Address.parse(textList));
|
|
}
|
|
|
|
/**
|
|
* Returns null if the packedList has 0 addresses, otherwise returns the first address.
|
|
* The same as Address.unpack(packedList)[0] for non-empty list.
|
|
* This is an utility method that offers some performance optimization opportunities.
|
|
*/
|
|
public static Address unpackFirst(String packedList) {
|
|
Address[] array = unpack(packedList);
|
|
return array.length > 0 ? array[0] : null;
|
|
}
|
|
|
|
/**
|
|
* Convert a packed list of addresses to a form suitable for use in an RFC822 header.
|
|
* This implementation is brute-force, and could be replaced with a more efficient version
|
|
* if desired.
|
|
*/
|
|
public static String packedToHeader(String packedList) {
|
|
return toHeader(unpack(packedList));
|
|
}
|
|
|
|
/**
|
|
* Unpacks an address list previously packed with pack()
|
|
* @param addressList String with packed addresses as returned by pack()
|
|
* @return array of addresses resulting from unpack
|
|
*/
|
|
public static Address[] unpack(String addressList) {
|
|
if (addressList == null || addressList.length() == 0) {
|
|
return EMPTY_ADDRESS_ARRAY;
|
|
}
|
|
ArrayList<Address> addresses = new ArrayList<Address>();
|
|
int length = addressList.length();
|
|
int pairStartIndex = 0;
|
|
int pairEndIndex = 0;
|
|
|
|
/* addressEndIndex is only re-scanned (indexOf()) when a LIST_DELIMITER_PERSONAL
|
|
is used, not for every email address; i.e. not for every iteration of the while().
|
|
This reduces the theoretical complexity from quadratic to linear,
|
|
and provides some speed-up in practice by removing redundant scans of the string.
|
|
*/
|
|
int addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL);
|
|
|
|
while (pairStartIndex < length) {
|
|
pairEndIndex = addressList.indexOf(LIST_DELIMITER_EMAIL, pairStartIndex);
|
|
if (pairEndIndex == -1) {
|
|
pairEndIndex = length;
|
|
}
|
|
Address address;
|
|
if (addressEndIndex == -1 || pairEndIndex <= addressEndIndex) {
|
|
// in this case the DELIMITER_PERSONAL is in a future pair,
|
|
// so don't use personal, and don't update addressEndIndex
|
|
address = new Address(addressList.substring(pairStartIndex, pairEndIndex), null);
|
|
} else {
|
|
address = new Address(addressList.substring(pairStartIndex, addressEndIndex),
|
|
addressList.substring(addressEndIndex + 1, pairEndIndex));
|
|
// only update addressEndIndex when we use the LIST_DELIMITER_PERSONAL
|
|
addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL, pairEndIndex + 1);
|
|
}
|
|
addresses.add(address);
|
|
pairStartIndex = pairEndIndex + 1;
|
|
}
|
|
return addresses.toArray(EMPTY_ADDRESS_ARRAY);
|
|
}
|
|
|
|
/**
|
|
* Packs an address list into a String that is very quick to read
|
|
* and parse. Packed lists can be unpacked with unpack().
|
|
* The format is a series of packed addresses separated by LIST_DELIMITER_EMAIL.
|
|
* Each address is packed as
|
|
* a pair of address and personal separated by LIST_DELIMITER_PERSONAL,
|
|
* where the personal and delimiter are optional.
|
|
* E.g. "foo@x.com\1joe@x.com\2Joe Doe"
|
|
* @param addresses Array of addresses
|
|
* @return a string containing the packed addresses.
|
|
*/
|
|
public static String pack(Address[] addresses) {
|
|
// TODO: return same value for both null & empty list
|
|
if (addresses == null) {
|
|
return null;
|
|
}
|
|
final int nAddr = addresses.length;
|
|
if (nAddr == 0) {
|
|
return "";
|
|
}
|
|
|
|
// shortcut: one email with no displayName
|
|
if (nAddr == 1 && addresses[0].getPersonal() == null) {
|
|
return addresses[0].getAddress();
|
|
}
|
|
|
|
StringBuffer sb = new StringBuffer();
|
|
for (int i = 0; i < nAddr; i++) {
|
|
if (i != 0) {
|
|
sb.append(LIST_DELIMITER_EMAIL);
|
|
}
|
|
final Address address = addresses[i];
|
|
sb.append(address.getAddress());
|
|
final String displayName = address.getPersonal();
|
|
if (displayName != null) {
|
|
sb.append(LIST_DELIMITER_PERSONAL);
|
|
sb.append(displayName);
|
|
}
|
|
}
|
|
return sb.toString();
|
|
}
|
|
|
|
/**
|
|
* Produces the same result as pack(array), but only packs one (this) address.
|
|
*/
|
|
public String pack() {
|
|
final String address = getAddress();
|
|
final String personal = getPersonal();
|
|
if (personal == null) {
|
|
return address;
|
|
} else {
|
|
return address + LIST_DELIMITER_PERSONAL + personal;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Legacy unpack() used for reading the old data (migration),
|
|
* as found in LocalStore (Donut; db version up to 24).
|
|
* @See unpack()
|
|
*/
|
|
public static Address[] legacyUnpack(String addressList) {
|
|
if (addressList == null || addressList.length() == 0) {
|
|
return new Address[] { };
|
|
}
|
|
ArrayList<Address> addresses = new ArrayList<Address>();
|
|
int length = addressList.length();
|
|
int pairStartIndex = 0;
|
|
int pairEndIndex = 0;
|
|
int addressEndIndex = 0;
|
|
while (pairStartIndex < length) {
|
|
pairEndIndex = addressList.indexOf(',', pairStartIndex);
|
|
if (pairEndIndex == -1) {
|
|
pairEndIndex = length;
|
|
}
|
|
addressEndIndex = addressList.indexOf(';', pairStartIndex);
|
|
String address = null;
|
|
String personal = null;
|
|
if (addressEndIndex == -1 || addressEndIndex > pairEndIndex) {
|
|
address =
|
|
Utility.fastUrlDecode(addressList.substring(pairStartIndex, pairEndIndex));
|
|
}
|
|
else {
|
|
address =
|
|
Utility.fastUrlDecode(addressList.substring(pairStartIndex, addressEndIndex));
|
|
personal =
|
|
Utility.fastUrlDecode(addressList.substring(addressEndIndex + 1, pairEndIndex));
|
|
}
|
|
addresses.add(new Address(address, personal));
|
|
pairStartIndex = pairEndIndex + 1;
|
|
}
|
|
return addresses.toArray(new Address[] { });
|
|
}
|
|
|
|
/**
|
|
* Legacy pack() used for writing to old data (migration),
|
|
* as found in LocalStore (Donut; db version up to 24).
|
|
* @See unpack()
|
|
*/
|
|
public static String legacyPack(Address[] addresses) {
|
|
if (addresses == null) {
|
|
return null;
|
|
} else if (addresses.length == 0) {
|
|
return "";
|
|
}
|
|
StringBuffer sb = new StringBuffer();
|
|
for (int i = 0, count = addresses.length; i < count; i++) {
|
|
Address address = addresses[i];
|
|
try {
|
|
sb.append(URLEncoder.encode(address.getAddress(), "UTF-8"));
|
|
if (address.getPersonal() != null) {
|
|
sb.append(';');
|
|
sb.append(URLEncoder.encode(address.getPersonal(), "UTF-8"));
|
|
}
|
|
if (i < count - 1) {
|
|
sb.append(',');
|
|
}
|
|
}
|
|
catch (UnsupportedEncodingException uee) {
|
|
return null;
|
|
}
|
|
}
|
|
return sb.toString();
|
|
}
|
|
}
|