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