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