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