• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.email.mail;
18 
19 import com.android.email.Utility;
20 
21 import org.apache.james.mime4j.codec.EncoderUtil;
22 import org.apache.james.mime4j.decoder.DecoderUtil;
23 
24 import android.text.TextUtils;
25 import android.text.util.Rfc822Token;
26 import android.text.util.Rfc822Tokenizer;
27 
28 import java.io.UnsupportedEncodingException;
29 import java.net.URLEncoder;
30 import java.util.ArrayList;
31 import java.util.regex.Pattern;
32 
33 /**
34  * This class represent email address.
35  *
36  * RFC822 email address may have following format.
37  *   "name" <address> (comment)
38  *   "name" <address>
39  *   name <address>
40  *   address
41  * Name and comment part should be MIME/base64 encoded in header if necessary.
42  *
43  */
44 public class Address {
45     /**
46      *  Address part, in the form local_part@domain_part. No surrounding angle brackets.
47      */
48     private String mAddress;
49 
50     /**
51      * Name part. No surrounding double quote, and no MIME/base64 encoding.
52      * This must be null if Address has no name part.
53      */
54     private String mPersonal;
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     private static final Address[] EMPTY_ADDRESS_ARRAY = new Address[0];
64 
65     // delimiters are chars that do not appear in an email address, used by pack/unpack
66     private static final char LIST_DELIMITER_EMAIL = '\1';
67     private static final char LIST_DELIMITER_PERSONAL = '\2';
68 
Address(String address, String personal)69     public Address(String address, String personal) {
70         setAddress(address);
71         setPersonal(personal);
72     }
73 
Address(String address)74     public Address(String address) {
75         setAddress(address);
76     }
77 
getAddress()78     public String getAddress() {
79         return mAddress;
80     }
81 
setAddress(String address)82     public void setAddress(String address) {
83         this.mAddress = REMOVE_OPTIONAL_BRACKET.matcher(address).replaceAll("$1");;
84     }
85 
86     /**
87      * Get name part as UTF-16 string. No surrounding double quote, and no MIME/base64 encoding.
88      *
89      * @return Name part of email address. Returns null if it is omitted.
90      */
getPersonal()91     public String getPersonal() {
92         return mPersonal;
93     }
94 
95     /**
96      * Set name part from UTF-16 string. Optional surrounding double quote will be removed.
97      * It will be also unquoted and MIME/base64 decoded.
98      *
99      * @param Personal name part of email address as UTF-16 string. Null is acceptable.
100      */
setPersonal(String personal)101     public void setPersonal(String personal) {
102         if (personal != null) {
103             personal = REMOVE_OPTIONAL_DQUOTE.matcher(personal).replaceAll("$1");
104             personal = UNQUOTE.matcher(personal).replaceAll("$1");
105             personal = DecoderUtil.decodeEncodedWords(personal);
106             if (personal.length() == 0) {
107                 personal = null;
108             }
109         }
110         this.mPersonal = personal;
111     }
112 
113     /**
114      * This method is used to check that all the addresses that the user
115      * entered in a list (e.g. To:) are valid, so that none is dropped.
116      */
isAllValid(String addressList)117     public static boolean isAllValid(String addressList) {
118         // This code mimics the parse() method below.
119         // I don't know how to better avoid the code-duplication.
120         if (addressList != null && addressList.length() > 0) {
121             Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
122             for (int i = 0, length = tokens.length; i < length; ++i) {
123                 Rfc822Token token = tokens[i];
124                 String address = token.getAddress();
125                 if (!TextUtils.isEmpty(address) && !isValidAddress(address)) {
126                     return false;
127                 }
128             }
129         }
130         return true;
131     }
132 
133     /**
134      * Parse a comma-delimited list of addresses in RFC822 format and return an
135      * array of Address objects.
136      *
137      * @param addressList Address list in comma-delimited string.
138      * @return An array of 0 or more Addresses.
139      */
parse(String addressList)140     public static Address[] parse(String addressList) {
141         if (addressList == null || addressList.length() == 0) {
142             return EMPTY_ADDRESS_ARRAY;
143         }
144         Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
145         ArrayList<Address> addresses = new ArrayList<Address>();
146         for (int i = 0, length = tokens.length; i < length; ++i) {
147             Rfc822Token token = tokens[i];
148             String address = token.getAddress();
149             if (!TextUtils.isEmpty(address)) {
150                 if (isValidAddress(address)) {
151                     String name = token.getName();
152                     if (TextUtils.isEmpty(name)) {
153                         name = null;
154                     }
155                     addresses.add(new Address(address, name));
156                 }
157             }
158         }
159         return addresses.toArray(new Address[] {});
160     }
161 
162     /**
163      * Checks whether a string email address is valid.
164      * E.g. name@domain.com is valid.
165      */
isValidAddress(String address)166     /* package */ static boolean isValidAddress(String address) {
167         // Note: Some email provider may violate the standard, so here we only check that
168         // address consists of two part that are separated by '@', and domain part contains
169         // at least one '.'.
170         int len = address.length();
171         int firstAt = address.indexOf('@');
172         int lastAt = address.lastIndexOf('@');
173         int firstDot = address.indexOf('.', lastAt + 1);
174         int lastDot = address.lastIndexOf('.');
175         return firstAt > 0 && firstAt == lastAt && lastAt + 1 < firstDot
176             && firstDot <= lastDot && lastDot < len - 1;
177     }
178 
179     @Override
equals(Object o)180     public boolean equals(Object o) {
181         if (o instanceof Address) {
182             // It seems that the spec says that the "user" part is case-sensitive,
183             // while the domain part in case-insesitive.
184             // So foo@yahoo.com and Foo@yahoo.com are different.
185             // This may seem non-intuitive from the user POV, so we
186             // may re-consider it if it creates UI trouble.
187             // A problem case is "replyAll" sending to both
188             // a@b.c and to A@b.c, which turn out to be the same on the server.
189             // Leave unchanged for now (i.e. case-sensitive).
190             return getAddress().equals(((Address) o).getAddress());
191         }
192         return super.equals(o);
193     }
194 
195     /**
196      * Get human readable address string.
197      * Do not use this for email header.
198      *
199      * @return Human readable address string.  Not quoted and not encoded.
200      */
201     @Override
toString()202     public String toString() {
203         if (mPersonal != null) {
204             if (mPersonal.matches(".*[\\(\\)<>@,;:\\\\\".\\[\\]].*")) {
205                 return Utility.quoteString(mPersonal) + " <" + mAddress + ">";
206             } else {
207                 return mPersonal + " <" + mAddress + ">";
208             }
209         } else {
210             return mAddress;
211         }
212     }
213 
214     /**
215      * Get human readable comma-delimited address string.
216      *
217      * @param addresses Address array
218      * @return Human readable comma-delimited address string.
219      */
toString(Address[] addresses)220     public static String toString(Address[] addresses) {
221         if (addresses == null || addresses.length == 0) {
222             return null;
223         }
224         if (addresses.length == 1) {
225             return addresses[0].toString();
226         }
227         StringBuffer sb = new StringBuffer(addresses[0].toString());
228         for (int i = 1; i < addresses.length; i++) {
229             sb.append(',');
230             sb.append(addresses[i].toString());
231         }
232         return sb.toString();
233     }
234 
235     /**
236      * Get RFC822/MIME compatible address string.
237      *
238      * @return RFC822/MIME compatible address string.
239      * It may be surrounded by double quote or quoted and MIME/base64 encoded if necessary.
240      */
toHeader()241     public String toHeader() {
242         if (mPersonal != null) {
243             return EncoderUtil.encodeAddressDisplayName(mPersonal) + " <" + mAddress + ">";
244         } else {
245             return mAddress;
246         }
247     }
248 
249     /**
250      * Get RFC822/MIME compatible comma-delimited address string.
251      *
252      * @param addresses Address array
253      * @return RFC822/MIME compatible comma-delimited address string.
254      * it may be surrounded by double quoted or quoted and MIME/base64 encoded if necessary.
255      */
toHeader(Address[] addresses)256     public static String toHeader(Address[] addresses) {
257         if (addresses == null || addresses.length == 0) {
258             return null;
259         }
260         if (addresses.length == 1) {
261             return addresses[0].toHeader();
262         }
263         StringBuffer sb = new StringBuffer(addresses[0].toHeader());
264         for (int i = 1; i < addresses.length; i++) {
265             // We need space character to be able to fold line.
266             sb.append(", ");
267             sb.append(addresses[i].toHeader());
268         }
269         return sb.toString();
270     }
271 
272     /**
273      * Get Human friendly address string.
274      *
275      * @return the personal part of this Address, or the address part if the
276      * personal part is not available
277      */
toFriendly()278     public String toFriendly() {
279         if (mPersonal != null && mPersonal.length() > 0) {
280             return mPersonal;
281         } else {
282             return mAddress;
283         }
284     }
285 
286     /**
287      * Creates a comma-delimited list of addresses in the "friendly" format (see toFriendly() for
288      * details on the per-address conversion).
289      *
290      * @param addresses Array of Address[] values
291      * @return A comma-delimited string listing all of the addresses supplied.  Null if source
292      * was null or empty.
293      */
toFriendly(Address[] addresses)294     public static String toFriendly(Address[] addresses) {
295         if (addresses == null || addresses.length == 0) {
296             return null;
297         }
298         if (addresses.length == 1) {
299             return addresses[0].toFriendly();
300         }
301         StringBuffer sb = new StringBuffer(addresses[0].toFriendly());
302         for (int i = 1; i < addresses.length; i++) {
303             sb.append(',');
304             sb.append(addresses[i].toFriendly());
305         }
306         return sb.toString();
307     }
308 
309     /**
310      * Returns exactly the same result as Address.toString(Address.unpack(packedList)).
311      */
unpackToString(String packedList)312     public static String unpackToString(String packedList) {
313         return toString(unpack(packedList));
314     }
315 
316     /**
317      * Returns exactly the same result as Address.pack(Address.parse(textList)).
318      */
parseAndPack(String textList)319     public static String parseAndPack(String textList) {
320         return Address.pack(Address.parse(textList));
321     }
322 
323     /**
324      * Returns null if the packedList has 0 addresses, otherwise returns the first address.
325      * The same as Address.unpack(packedList)[0] for non-empty list.
326      * This is an utility method that offers some performance optimization opportunities.
327      */
unpackFirst(String packedList)328     public static Address unpackFirst(String packedList) {
329         Address[] array = unpack(packedList);
330         return array.length > 0 ? array[0] : null;
331     }
332 
333     /**
334      * Convert a packed list of addresses to a form suitable for use in an RFC822 header.
335      * This implementation is brute-force, and could be replaced with a more efficient version
336      * if desired.
337      */
packedToHeader(String packedList)338     public static String packedToHeader(String packedList) {
339         return toHeader(unpack(packedList));
340     }
341 
342     /**
343      * Unpacks an address list previously packed with pack()
344      * @param addressList String with packed addresses as returned by pack()
345      * @return array of addresses resulting from unpack
346      */
unpack(String addressList)347     public static Address[] unpack(String addressList) {
348         if (addressList == null || addressList.length() == 0) {
349             return EMPTY_ADDRESS_ARRAY;
350         }
351         ArrayList<Address> addresses = new ArrayList<Address>();
352         int length = addressList.length();
353         int pairStartIndex = 0;
354         int pairEndIndex = 0;
355 
356         /* addressEndIndex is only re-scanned (indexOf()) when a LIST_DELIMITER_PERSONAL
357            is used, not for every email address; i.e. not for every iteration of the while().
358            This reduces the theoretical complexity from quadratic to linear,
359            and provides some speed-up in practice by removing redundant scans of the string.
360         */
361         int addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL);
362 
363         while (pairStartIndex < length) {
364             pairEndIndex = addressList.indexOf(LIST_DELIMITER_EMAIL, pairStartIndex);
365             if (pairEndIndex == -1) {
366                 pairEndIndex = length;
367             }
368             Address address;
369             if (addressEndIndex == -1 || pairEndIndex <= addressEndIndex) {
370                 // in this case the DELIMITER_PERSONAL is in a future pair,
371                 // so don't use personal, and don't update addressEndIndex
372                 address = new Address(addressList.substring(pairStartIndex, pairEndIndex), null);
373             } else {
374                 address = new Address(addressList.substring(pairStartIndex, addressEndIndex),
375                                       addressList.substring(addressEndIndex + 1, pairEndIndex));
376                 // only update addressEndIndex when we use the LIST_DELIMITER_PERSONAL
377                 addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL, pairEndIndex + 1);
378             }
379             addresses.add(address);
380             pairStartIndex = pairEndIndex + 1;
381         }
382         return addresses.toArray(EMPTY_ADDRESS_ARRAY);
383     }
384 
385     /**
386      * Packs an address list into a String that is very quick to read
387      * and parse. Packed lists can be unpacked with unpack().
388      * The format is a series of packed addresses separated by LIST_DELIMITER_EMAIL.
389      * Each address is packed as
390      * a pair of address and personal separated by LIST_DELIMITER_PERSONAL,
391      * where the personal and delimiter are optional.
392      * E.g. "foo@x.com\1joe@x.com\2Joe Doe"
393      * @param addresses Array of addresses
394      * @return a string containing the packed addresses.
395      */
pack(Address[] addresses)396     public static String pack(Address[] addresses) {
397         // TODO: return same value for both null & empty list
398         if (addresses == null) {
399             return null;
400         }
401         final int nAddr = addresses.length;
402         if (nAddr == 0) {
403             return "";
404         }
405 
406         // shortcut: one email with no displayName
407         if (nAddr == 1 && addresses[0].getPersonal() == null) {
408             return addresses[0].getAddress();
409         }
410 
411         StringBuffer sb = new StringBuffer();
412         for (int i = 0; i < nAddr; i++) {
413             if (i != 0) {
414                 sb.append(LIST_DELIMITER_EMAIL);
415             }
416             final Address address = addresses[i];
417             sb.append(address.getAddress());
418             final String displayName = address.getPersonal();
419             if (displayName != null) {
420                 sb.append(LIST_DELIMITER_PERSONAL);
421                 sb.append(displayName);
422             }
423         }
424         return sb.toString();
425     }
426 
427     /**
428      * Produces the same result as pack(array), but only packs one (this) address.
429      */
pack()430     public String pack() {
431         final String address = getAddress();
432         final String personal = getPersonal();
433         if (personal == null) {
434             return address;
435         } else {
436             return address + LIST_DELIMITER_PERSONAL + personal;
437         }
438     }
439 
440     /**
441      * Legacy unpack() used for reading the old data (migration),
442      * as found in LocalStore (Donut; db version up to 24).
443      * @See unpack()
444      */
legacyUnpack(String addressList)445     public static Address[] legacyUnpack(String addressList) {
446         if (addressList == null || addressList.length() == 0) {
447             return new Address[] { };
448         }
449         ArrayList<Address> addresses = new ArrayList<Address>();
450         int length = addressList.length();
451         int pairStartIndex = 0;
452         int pairEndIndex = 0;
453         int addressEndIndex = 0;
454         while (pairStartIndex < length) {
455             pairEndIndex = addressList.indexOf(',', pairStartIndex);
456             if (pairEndIndex == -1) {
457                 pairEndIndex = length;
458             }
459             addressEndIndex = addressList.indexOf(';', pairStartIndex);
460             String address = null;
461             String personal = null;
462             if (addressEndIndex == -1 || addressEndIndex > pairEndIndex) {
463                 address =
464                     Utility.fastUrlDecode(addressList.substring(pairStartIndex, pairEndIndex));
465             }
466             else {
467                 address =
468                     Utility.fastUrlDecode(addressList.substring(pairStartIndex, addressEndIndex));
469                 personal =
470                     Utility.fastUrlDecode(addressList.substring(addressEndIndex + 1, pairEndIndex));
471             }
472             addresses.add(new Address(address, personal));
473             pairStartIndex = pairEndIndex + 1;
474         }
475         return addresses.toArray(new Address[] { });
476     }
477 
478     /**
479      * Legacy pack() used for writing to old data (migration),
480      * as found in LocalStore (Donut; db version up to 24).
481      * @See unpack()
482      */
legacyPack(Address[] addresses)483     public static String legacyPack(Address[] addresses) {
484         if (addresses == null) {
485             return null;
486         } else if (addresses.length == 0) {
487             return "";
488         }
489         StringBuffer sb = new StringBuffer();
490         for (int i = 0, count = addresses.length; i < count; i++) {
491             Address address = addresses[i];
492             try {
493                 sb.append(URLEncoder.encode(address.getAddress(), "UTF-8"));
494                 if (address.getPersonal() != null) {
495                     sb.append(';');
496                     sb.append(URLEncoder.encode(address.getPersonal(), "UTF-8"));
497                 }
498                 if (i < count - 1) {
499                     sb.append(',');
500                 }
501             }
502             catch (UnsupportedEncodingException uee) {
503                 return null;
504             }
505         }
506         return sb.toString();
507     }
508 }
509