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