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