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      */
isValidEmail(final String emailText)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@example.org>"</code> won't be valid.
46      */
EmailAddress(final String emailString)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@example.org>"</code> won't be valid.
56      * @param i18n Accept an internationalized address if it is true.
57      */
EmailAddress(final String emailString, final boolean i18n)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      */
EmailAddress(final String user, final String host)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      */
EmailAddress(final String user, final String host, final boolean i18n)80     public EmailAddress(final String user, final String host, final boolean i18n) {
81         allowI18n = i18n;
82         this.user = user;
83         setHost(host);
84     }
85 
parseEmail(final String emailString)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
toString()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      */
isValidInternal()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 
isQuotedUserValid()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
equals(final Object otherObject)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
hashCode()235     public int hashCode() {
236         // Arbitrary hash code as a function of both host and user.
237         return toString().hashCode();
238     }
239 
240     // accessors
isValid()241     public boolean isValid() {
242         return valid;
243     }
244 
getUser()245     public String getUser() {
246         return user;
247     }
248 
getHost()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      */
setHost(final String hostName)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