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.email.mail;
     18 
     19 import com.android.email.Utility;
     20 
     21 import org.apache.james.mime4j.codec.EncoderUtil;
     22 import org.apache.james.mime4j.decoder.DecoderUtil;
     23 
     24 import android.text.TextUtils;
     25 import android.text.util.Rfc822Token;
     26 import android.text.util.Rfc822Tokenizer;
     27 
     28 import java.io.UnsupportedEncodingException;
     29 import java.net.URLEncoder;
     30 import java.util.ArrayList;
     31 import java.util.regex.Pattern;
     32 
     33 /**
     34  * This class represent email address.
     35  *
     36  * RFC822 email address may have following format.
     37  *   "name" <address> (comment)
     38  *   "name" <address>
     39  *   name <address>
     40  *   address
     41  * Name and comment part should be MIME/base64 encoded in header if necessary.
     42  *
     43  */
     44 public class Address {
     45     /**
     46      *  Address part, in the form local_part@domain_part. No surrounding angle brackets.
     47      */
     48     private String mAddress;
     49 
     50     /**
     51      * Name part. No surrounding double quote, and no MIME/base64 encoding.
     52      * This must be null if Address has no name part.
     53      */
     54     private String mPersonal;
     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     private static final Address[] EMPTY_ADDRESS_ARRAY = new Address[0];
     64 
     65     // delimiters are chars that do not appear in an email address, used by pack/unpack
     66     private static final char LIST_DELIMITER_EMAIL = '\1';
     67     private static final char LIST_DELIMITER_PERSONAL = '\2';
     68 
     69     public Address(String address, String personal) {
     70         setAddress(address);
     71         setPersonal(personal);
     72     }
     73 
     74     public Address(String address) {
     75         setAddress(address);
     76     }
     77 
     78     public String getAddress() {
     79         return mAddress;
     80     }
     81 
     82     public void setAddress(String address) {
     83         this.mAddress = REMOVE_OPTIONAL_BRACKET.matcher(address).replaceAll("$1");;
     84     }
     85 
     86     /**
     87      * Get name part as UTF-16 string. No surrounding double quote, and no MIME/base64 encoding.
     88      *
     89      * @return Name part of email address. Returns null if it is omitted.
     90      */
     91     public String getPersonal() {
     92         return mPersonal;
     93     }
     94 
     95     /**
     96      * Set name part from UTF-16 string. Optional surrounding double quote will be removed.
     97      * It will be also unquoted and MIME/base64 decoded.
     98      *
     99      * @param Personal name part of email address as UTF-16 string. Null is acceptable.
    100      */
    101     public void setPersonal(String personal) {
    102         if (personal != null) {
    103             personal = REMOVE_OPTIONAL_DQUOTE.matcher(personal).replaceAll("$1");
    104             personal = UNQUOTE.matcher(personal).replaceAll("$1");
    105             personal = DecoderUtil.decodeEncodedWords(personal);
    106             if (personal.length() == 0) {
    107                 personal = null;
    108             }
    109         }
    110         this.mPersonal = personal;
    111     }
    112 
    113     /**
    114      * This method is used to check that all the addresses that the user
    115      * entered in a list (e.g. To:) are valid, so that none is dropped.
    116      */
    117     public static boolean isAllValid(String addressList) {
    118         // This code mimics the parse() method below.
    119         // I don't know how to better avoid the code-duplication.
    120         if (addressList != null && addressList.length() > 0) {
    121             Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
    122             for (int i = 0, length = tokens.length; i < length; ++i) {
    123                 Rfc822Token token = tokens[i];
    124                 String address = token.getAddress();
    125                 if (!TextUtils.isEmpty(address) && !isValidAddress(address)) {
    126                     return false;
    127                 }
    128             }
    129         }
    130         return true;
    131     }
    132 
    133     /**
    134      * Parse a comma-delimited list of addresses in RFC822 format and return an
    135      * array of Address objects.
    136      *
    137      * @param addressList Address list in comma-delimited string.
    138      * @return An array of 0 or more Addresses.
    139      */
    140     public static Address[] parse(String addressList) {
    141         if (addressList == null || addressList.length() == 0) {
    142             return EMPTY_ADDRESS_ARRAY;
    143         }
    144         Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
    145         ArrayList<Address> addresses = new ArrayList<Address>();
    146         for (int i = 0, length = tokens.length; i < length; ++i) {
    147             Rfc822Token token = tokens[i];
    148             String address = token.getAddress();
    149             if (!TextUtils.isEmpty(address)) {
    150                 if (isValidAddress(address)) {
    151                     String name = token.getName();
    152                     if (TextUtils.isEmpty(name)) {
    153                         name = null;
    154                     }
    155                     addresses.add(new Address(address, name));
    156                 }
    157             }
    158         }
    159         return addresses.toArray(new Address[] {});
    160     }
    161 
    162     /**
    163      * Checks whether a string email address is valid.
    164      * E.g. name (at) domain.com is valid.
    165      */
    166     /* package */ 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) {
    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         if (addresses == null || addresses.length == 0) {
    222             return null;
    223         }
    224         if (addresses.length == 1) {
    225             return addresses[0].toString();
    226         }
    227         StringBuffer sb = new StringBuffer(addresses[0].toString());
    228         for (int i = 1; i < addresses.length; i++) {
    229             sb.append(',');
    230             sb.append(addresses[i].toString());
    231         }
    232         return sb.toString();
    233     }
    234 
    235     /**
    236      * Get RFC822/MIME compatible address string.
    237      *
    238      * @return RFC822/MIME compatible address string.
    239      * It may be surrounded by double quote or quoted and MIME/base64 encoded if necessary.
    240      */
    241     public String toHeader() {
    242         if (mPersonal != null) {
    243             return EncoderUtil.encodeAddressDisplayName(mPersonal) + " <" + mAddress + ">";
    244         } else {
    245             return mAddress;
    246         }
    247     }
    248 
    249     /**
    250      * Get RFC822/MIME compatible comma-delimited address string.
    251      *
    252      * @param addresses Address array
    253      * @return RFC822/MIME compatible comma-delimited address string.
    254      * it may be surrounded by double quoted or quoted and MIME/base64 encoded if necessary.
    255      */
    256     public static String toHeader(Address[] addresses) {
    257         if (addresses == null || addresses.length == 0) {
    258             return null;
    259         }
    260         if (addresses.length == 1) {
    261             return addresses[0].toHeader();
    262         }
    263         StringBuffer sb = new StringBuffer(addresses[0].toHeader());
    264         for (int i = 1; i < addresses.length; i++) {
    265             // We need space character to be able to fold line.
    266             sb.append(", ");
    267             sb.append(addresses[i].toHeader());
    268         }
    269         return sb.toString();
    270     }
    271 
    272     /**
    273      * Get Human friendly address string.
    274      *
    275      * @return the personal part of this Address, or the address part if the
    276      * personal part is not available
    277      */
    278     public String toFriendly() {
    279         if (mPersonal != null && mPersonal.length() > 0) {
    280             return mPersonal;
    281         } else {
    282             return mAddress;
    283         }
    284     }
    285 
    286     /**
    287      * Creates a comma-delimited list of addresses in the "friendly" format (see toFriendly() for
    288      * details on the per-address conversion).
    289      *
    290      * @param addresses Array of Address[] values
    291      * @return A comma-delimited string listing all of the addresses supplied.  Null if source
    292      * was null or empty.
    293      */
    294     public static String toFriendly(Address[] addresses) {
    295         if (addresses == null || addresses.length == 0) {
    296             return null;
    297         }
    298         if (addresses.length == 1) {
    299             return addresses[0].toFriendly();
    300         }
    301         StringBuffer sb = new StringBuffer(addresses[0].toFriendly());
    302         for (int i = 1; i < addresses.length; i++) {
    303             sb.append(',');
    304             sb.append(addresses[i].toFriendly());
    305         }
    306         return sb.toString();
    307     }
    308 
    309     /**
    310      * Returns exactly the same result as Address.toString(Address.unpack(packedList)).
    311      */
    312     public static String unpackToString(String packedList) {
    313         return toString(unpack(packedList));
    314     }
    315 
    316     /**
    317      * Returns exactly the same result as Address.pack(Address.parse(textList)).
    318      */
    319     public static String parseAndPack(String textList) {
    320         return Address.pack(Address.parse(textList));
    321     }
    322 
    323     /**
    324      * Returns null if the packedList has 0 addresses, otherwise returns the first address.
    325      * The same as Address.unpack(packedList)[0] for non-empty list.
    326      * This is an utility method that offers some performance optimization opportunities.
    327      */
    328     public static Address unpackFirst(String packedList) {
    329         Address[] array = unpack(packedList);
    330         return array.length > 0 ? array[0] : null;
    331     }
    332 
    333     /**
    334      * Convert a packed list of addresses to a form suitable for use in an RFC822 header.
    335      * This implementation is brute-force, and could be replaced with a more efficient version
    336      * if desired.
    337      */
    338     public static String packedToHeader(String packedList) {
    339         return toHeader(unpack(packedList));
    340     }
    341 
    342     /**
    343      * Unpacks an address list previously packed with pack()
    344      * @param addressList String with packed addresses as returned by pack()
    345      * @return array of addresses resulting from unpack
    346      */
    347     public static Address[] unpack(String addressList) {
    348         if (addressList == null || addressList.length() == 0) {
    349             return EMPTY_ADDRESS_ARRAY;
    350         }
    351         ArrayList<Address> addresses = new ArrayList<Address>();
    352         int length = addressList.length();
    353         int pairStartIndex = 0;
    354         int pairEndIndex = 0;
    355 
    356         /* addressEndIndex is only re-scanned (indexOf()) when a LIST_DELIMITER_PERSONAL
    357            is used, not for every email address; i.e. not for every iteration of the while().
    358            This reduces the theoretical complexity from quadratic to linear,
    359            and provides some speed-up in practice by removing redundant scans of the string.
    360         */
    361         int addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL);
    362 
    363         while (pairStartIndex < length) {
    364             pairEndIndex = addressList.indexOf(LIST_DELIMITER_EMAIL, pairStartIndex);
    365             if (pairEndIndex == -1) {
    366                 pairEndIndex = length;
    367             }
    368             Address address;
    369             if (addressEndIndex == -1 || pairEndIndex <= addressEndIndex) {
    370                 // in this case the DELIMITER_PERSONAL is in a future pair,
    371                 // so don't use personal, and don't update addressEndIndex
    372                 address = new Address(addressList.substring(pairStartIndex, pairEndIndex), null);
    373             } else {
    374                 address = new Address(addressList.substring(pairStartIndex, addressEndIndex),
    375                                       addressList.substring(addressEndIndex + 1, pairEndIndex));
    376                 // only update addressEndIndex when we use the LIST_DELIMITER_PERSONAL
    377                 addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL, pairEndIndex + 1);
    378             }
    379             addresses.add(address);
    380             pairStartIndex = pairEndIndex + 1;
    381         }
    382         return addresses.toArray(EMPTY_ADDRESS_ARRAY);
    383     }
    384 
    385     /**
    386      * Packs an address list into a String that is very quick to read
    387      * and parse. Packed lists can be unpacked with unpack().
    388      * The format is a series of packed addresses separated by LIST_DELIMITER_EMAIL.
    389      * Each address is packed as
    390      * a pair of address and personal separated by LIST_DELIMITER_PERSONAL,
    391      * where the personal and delimiter are optional.
    392      * E.g. "foo (at) x.com\1joe (at) x.com\2Joe Doe"
    393      * @param addresses Array of addresses
    394      * @return a string containing the packed addresses.
    395      */
    396     public static String pack(Address[] addresses) {
    397         // TODO: return same value for both null & empty list
    398         if (addresses == null) {
    399             return null;
    400         }
    401         final int nAddr = addresses.length;
    402         if (nAddr == 0) {
    403             return "";
    404         }
    405 
    406         // shortcut: one email with no displayName
    407         if (nAddr == 1 && addresses[0].getPersonal() == null) {
    408             return addresses[0].getAddress();
    409         }
    410 
    411         StringBuffer sb = new StringBuffer();
    412         for (int i = 0; i < nAddr; i++) {
    413             if (i != 0) {
    414                 sb.append(LIST_DELIMITER_EMAIL);
    415             }
    416             final Address address = addresses[i];
    417             sb.append(address.getAddress());
    418             final String displayName = address.getPersonal();
    419             if (displayName != null) {
    420                 sb.append(LIST_DELIMITER_PERSONAL);
    421                 sb.append(displayName);
    422             }
    423         }
    424         return sb.toString();
    425     }
    426 
    427     /**
    428      * Produces the same result as pack(array), but only packs one (this) address.
    429      */
    430     public String pack() {
    431         final String address = getAddress();
    432         final String personal = getPersonal();
    433         if (personal == null) {
    434             return address;
    435         } else {
    436             return address + LIST_DELIMITER_PERSONAL + personal;
    437         }
    438     }
    439 
    440     /**
    441      * Legacy unpack() used for reading the old data (migration),
    442      * as found in LocalStore (Donut; db version up to 24).
    443      * @See unpack()
    444      */
    445     public static Address[] legacyUnpack(String addressList) {
    446         if (addressList == null || addressList.length() == 0) {
    447             return new Address[] { };
    448         }
    449         ArrayList<Address> addresses = new ArrayList<Address>();
    450         int length = addressList.length();
    451         int pairStartIndex = 0;
    452         int pairEndIndex = 0;
    453         int addressEndIndex = 0;
    454         while (pairStartIndex < length) {
    455             pairEndIndex = addressList.indexOf(',', pairStartIndex);
    456             if (pairEndIndex == -1) {
    457                 pairEndIndex = length;
    458             }
    459             addressEndIndex = addressList.indexOf(';', pairStartIndex);
    460             String address = null;
    461             String personal = null;
    462             if (addressEndIndex == -1 || addressEndIndex > pairEndIndex) {
    463                 address =
    464                     Utility.fastUrlDecode(addressList.substring(pairStartIndex, pairEndIndex));
    465             }
    466             else {
    467                 address =
    468                     Utility.fastUrlDecode(addressList.substring(pairStartIndex, addressEndIndex));
    469                 personal =
    470                     Utility.fastUrlDecode(addressList.substring(addressEndIndex + 1, pairEndIndex));
    471             }
    472             addresses.add(new Address(address, personal));
    473             pairStartIndex = pairEndIndex + 1;
    474         }
    475         return addresses.toArray(new Address[] { });
    476     }
    477 
    478     /**
    479      * Legacy pack() used for writing to old data (migration),
    480      * as found in LocalStore (Donut; db version up to 24).
    481      * @See unpack()
    482      */
    483     public static String legacyPack(Address[] addresses) {
    484         if (addresses == null) {
    485             return null;
    486         } else if (addresses.length == 0) {
    487             return "";
    488         }
    489         StringBuffer sb = new StringBuffer();
    490         for (int i = 0, count = addresses.length; i < count; i++) {
    491             Address address = addresses[i];
    492             try {
    493                 sb.append(URLEncoder.encode(address.getAddress(), "UTF-8"));
    494                 if (address.getPersonal() != null) {
    495                     sb.append(';');
    496                     sb.append(URLEncoder.encode(address.getPersonal(), "UTF-8"));
    497                 }
    498                 if (i < count - 1) {
    499                     sb.append(',');
    500                 }
    501             }
    502             catch (UnsupportedEncodingException uee) {
    503                 return null;
    504             }
    505         }
    506         return sb.toString();
    507     }
    508 }
    509