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