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
17package com.android.messaging.util;
18
19import com.google.common.base.CharMatcher;
20
21/**
22 * Parsing the email address
23 */
24public 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@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@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