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