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