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