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