Home | History | Annotate | Download | only in util
      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 
     17 package com.android.messaging.util;
     18 
     19 import com.google.common.base.CharMatcher;
     20 
     21 /**
     22  * Parsing the email address
     23  */
     24 public final class EmailAddress {
     25     private static final CharMatcher ANY_WHITESPACE = CharMatcher.anyOf(
     26             " \t\n\r\f\u000B\u0085\u2028\u2029\u200D\uFFEF\uFFFD\uFFFE\uFFFF");
     27     private static final CharMatcher EMAIL_ALLOWED_CHARS = CharMatcher.inRange((char) 0, (char) 31)
     28             .or(CharMatcher.is((char) 127))
     29             .or(CharMatcher.anyOf(" @,:<>"))
     30             .negate();
     31 
     32     /**
     33      * Helper method that checks whether the input text is valid email address.
     34      * TODO: This creates a new EmailAddress object each time
     35      * Need to make it more lightweight by pulling out the validation code into a static method.
     36      */
     37     public static boolean isValidEmail(final String emailText) {
     38         return new EmailAddress(emailText).isValid();
     39     }
     40 
     41     /**
     42      * Parses the specified email address. Internationalized addresses are treated as invalid.
     43      *
     44      * @param emailString A string representing just an email address. It should
     45      * not contain any other tokens. <code>"Name&lt;foo (at) example.org>"</code> won't be valid.
     46      */
     47     public EmailAddress(final String emailString) {
     48         this(emailString, false);
     49     }
     50 
     51     /**
     52      * Parses the specified email address.
     53      *
     54      * @param emailString A string representing just an email address. It should
     55      * not contain any other tokens. <code>"Name&lt;foo (at) example.org>"</code> won't be valid.
     56      * @param i18n Accept an internationalized address if it is true.
     57      */
     58     public EmailAddress(final String emailString, final boolean i18n) {
     59         allowI18n = i18n;
     60         valid = parseEmail(emailString);
     61     }
     62 
     63     /**
     64      * Parses the specified email address. Internationalized addresses are treated as invalid.
     65      *
     66      * @param user A string representing the username in the email prior to the '@' symbol
     67      * @param host A string representing the host following the '@' symbol
     68      */
     69     public EmailAddress(final String user, final String host) {
     70         this(user, host, false);
     71     }
     72 
     73     /**
     74      * Parses the specified email address.
     75      *
     76      * @param user A string representing the username in the email prior to the '@' symbol
     77      * @param host A string representing the host following the '@' symbol
     78      * @param i18n Accept an internationalized address if it is true.
     79      */
     80     public EmailAddress(final String user, final String host, final boolean i18n) {
     81         allowI18n = i18n;
     82         this.user = user;
     83         setHost(host);
     84     }
     85 
     86     protected boolean parseEmail(final String emailString) {
     87         // check for null
     88         if (emailString == null) {
     89             return false;
     90         }
     91 
     92         // Check for an '@' character. Get the last one, in case the local part is
     93         // quoted. See http://b/1944742.
     94         final int atIndex = emailString.lastIndexOf('@');
     95         if ((atIndex <= 0) || // no '@' character in the email address
     96                               // or @ on the first position
     97                 (atIndex == (emailString.length() - 1))) { // last character, no host
     98             return false;
     99         }
    100 
    101         user = emailString.substring(0, atIndex);
    102         host = emailString.substring(atIndex + 1);
    103 
    104         return isValidInternal();
    105     }
    106 
    107     @Override
    108     public String toString() {
    109         return user + "@" + host;
    110     }
    111 
    112     /**
    113      * Ensure the email address is valid, conforming to current RFC2821 and
    114      * RFC2822 guidelines (although some iffy characters, like ! and ;, are
    115      * allowed because they are not technically prohibited in the RFC)
    116      */
    117     private boolean isValidInternal() {
    118         if ((user == null) || (host == null)) {
    119             return false;
    120         }
    121 
    122         if ((user.length() == 0) || (host.length() == 0)) {
    123             return false;
    124         }
    125 
    126         // check for white space in the host
    127         if (ANY_WHITESPACE.indexIn(host) >= 0) {
    128             return false;
    129         }
    130 
    131         // ensure the host is above the minimum length
    132         if (host.length() < 4) {
    133             return false;
    134         }
    135 
    136         final int firstDot = host.indexOf('.');
    137 
    138         // ensure host contains at least one dot
    139         if (firstDot == -1) {
    140             return false;
    141         }
    142 
    143         // check if the host contains two continuous dots.
    144         if (host.indexOf("..") >= 0) {
    145             return false;
    146         }
    147 
    148         // check if the first host char is a dot.
    149         if (host.charAt(0) == '.') {
    150             return false;
    151         }
    152 
    153         final int secondDot = host.indexOf(".", firstDot + 1);
    154 
    155         // if there's a dot at the end, there needs to be a second dot
    156         if (host.charAt(host.length() - 1) == '.' && secondDot == -1) {
    157             return false;
    158         }
    159 
    160         // Host must not have any disallowed characters; allowI18n dictates whether
    161         // host must be ASCII.
    162         if (!EMAIL_ALLOWED_CHARS.matchesAllOf(host)
    163                 || (!allowI18n && !CharMatcher.ASCII.matchesAllOf(host))) {
    164             return false;
    165         }
    166 
    167         if (user.startsWith("\"")) {
    168             if (!isQuotedUserValid()) {
    169                 return false;
    170             }
    171         } else {
    172             // check for white space in the user
    173             if (ANY_WHITESPACE.indexIn(user) >= 0) {
    174                 return false;
    175             }
    176 
    177             // the user cannot contain two continuous dots
    178             if (user.indexOf("..") >= 0) {
    179                 return false;
    180             }
    181 
    182             // User must not have any disallowed characters; allow I18n dictates whether
    183             // user must be ASCII.
    184             if (!EMAIL_ALLOWED_CHARS.matchesAllOf(user)
    185                     || (!allowI18n && !CharMatcher.ASCII.matchesAllOf(user))) {
    186                 return false;
    187             }
    188         }
    189         return true;
    190     }
    191 
    192     private boolean isQuotedUserValid() {
    193         final int limit = user.length() - 1;
    194         if (limit < 1 || !user.endsWith("\"")) {
    195             return false;
    196         }
    197 
    198         // Unusual loop bounds (looking only at characters between the outer quotes,
    199         // not at either quote character). Plus, i is manipulated within the loop.
    200         for (int i = 1; i < limit; ++i) {
    201             final char ch = user.charAt(i);
    202             if (ch == '"' || ch == 127
    203                     // No non-whitespace control chars:
    204                     || (ch < 32 && !ANY_WHITESPACE.matches(ch))
    205                     // No non-ASCII chars, unless i18n is in effect:
    206                     || (ch >= 128 && !allowI18n)) {
    207                 return false;
    208             } else if (ch == '\\') {
    209                 if (i + 1 < limit) {
    210                     ++i; // Skip the quoted character
    211                 } else {
    212                     // We have a trailing backslash -- so it can't be quoting anything.
    213                     return false;
    214                 }
    215             }
    216         }
    217 
    218         return true;
    219     }
    220 
    221     @Override
    222     public boolean equals(final Object otherObject) {
    223         // Do an instance check first as an optimization.
    224         if (this == otherObject) {
    225             return true;
    226         }
    227         if (otherObject instanceof EmailAddress) {
    228             final EmailAddress otherAddress = (EmailAddress) otherObject;
    229             return toString().equals(otherAddress.toString());
    230         }
    231         return false;
    232     }
    233 
    234     @Override
    235     public int hashCode() {
    236         // Arbitrary hash code as a function of both host and user.
    237         return toString().hashCode();
    238     }
    239 
    240     // accessors
    241     public boolean isValid() {
    242         return valid;
    243     }
    244 
    245     public String getUser() {
    246         return user;
    247     }
    248 
    249     public String getHost() {
    250         return host;
    251     }
    252 
    253     // used to change the host on an email address and rechecks validity
    254 
    255     /**
    256      * Changes the host name of the email address and rechecks the address'
    257      * validity. Exercise caution when storing EmailAddress instances in
    258      * hash-keyed collections. Calling setHost() with a different host name will
    259      * change the return value of hashCode.
    260      *
    261      * @param hostName The new host name of the email address.
    262      */
    263     public void setHost(final String hostName) {
    264         host = hostName;
    265         valid = isValidInternal();
    266     }
    267 
    268     protected boolean valid = false;
    269     protected String user = null;
    270     protected String host = null;
    271     protected boolean allowI18n = false;
    272 }
    273