• 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.emailcommon.mail;
18 
19 import android.text.TextUtils;
20 import android.text.util.Rfc822Token;
21 import android.text.util.Rfc822Tokenizer;
22 
23 import com.android.emailcommon.utility.Utility;
24 import com.google.common.annotations.VisibleForTesting;
25 
26 import org.apache.james.mime4j.codec.EncoderUtil;
27 import org.apache.james.mime4j.decoder.DecoderUtil;
28 
29 import java.util.ArrayList;
30 import java.util.regex.Pattern;
31 
32 /**
33  * This class represent email address.
34  *
35  * RFC822 email address may have following format.
36  *   "name" <address> (comment)
37  *   "name" <address>
38  *   name <address>
39  *   address
40  * Name and comment part should be MIME/base64 encoded in header if necessary.
41  *
42  */
43 public class Address {
44     /**
45      *  Address part, in the form local_part@domain_part. No surrounding angle brackets.
46      */
47     private String mAddress;
48 
49     /**
50      * Name part. No surrounding double quote, and no MIME/base64 encoding.
51      * This must be null if Address has no name part.
52      */
53     private String mPersonal;
54 
55     // Regex that matches address surrounded by '<>' optionally. '^<?([^>]+)>?$'
56     private static final Pattern REMOVE_OPTIONAL_BRACKET = Pattern.compile("^<?([^>]+)>?$");
57     // Regex that matches personal name surrounded by '""' optionally. '^"?([^"]+)"?$'
58     private static final Pattern REMOVE_OPTIONAL_DQUOTE = Pattern.compile("^\"?([^\"]*)\"?$");
59     // Regex that matches escaped character '\\([\\"])'
60     private static final Pattern UNQUOTE = Pattern.compile("\\\\([\\\\\"])");
61 
62     private static final Address[] EMPTY_ADDRESS_ARRAY = new Address[0];
63 
64     // delimiters are chars that do not appear in an email address, used by pack/unpack
65     private static final char LIST_DELIMITER_EMAIL = '\1';
66     private static final char LIST_DELIMITER_PERSONAL = '\2';
67 
Address(String address, String personal)68     public Address(String address, String personal) {
69         setAddress(address);
70         setPersonal(personal);
71     }
72 
Address(String address)73     public Address(String address) {
74         setAddress(address);
75     }
76 
getAddress()77     public String getAddress() {
78         return mAddress;
79     }
80 
setAddress(String address)81     public void setAddress(String address) {
82         mAddress = REMOVE_OPTIONAL_BRACKET.matcher(address).replaceAll("$1");
83     }
84 
85     /**
86      * Get name part as UTF-16 string. No surrounding double quote, and no MIME/base64 encoding.
87      *
88      * @return Name part of email address. Returns null if it is omitted.
89      */
getPersonal()90     public String getPersonal() {
91         return mPersonal;
92     }
93 
94     /**
95      * Set name part from UTF-16 string. Optional surrounding double quote will be removed.
96      * It will be also unquoted and MIME/base64 decoded.
97      *
98      * @param personal name part of email address as UTF-16 string. Null is acceptable.
99      */
setPersonal(String personal)100     public void setPersonal(String personal) {
101         if (personal != null) {
102             personal = REMOVE_OPTIONAL_DQUOTE.matcher(personal).replaceAll("$1");
103             personal = UNQUOTE.matcher(personal).replaceAll("$1");
104             personal = DecoderUtil.decodeEncodedWords(personal);
105             if (personal.length() == 0) {
106                 personal = null;
107             }
108         }
109         mPersonal = personal;
110     }
111 
112     /**
113      * This method is used to check that all the addresses that the user
114      * entered in a list (e.g. To:) are valid, so that none is dropped.
115      */
isAllValid(String addressList)116     public static boolean isAllValid(String addressList) {
117         // This code mimics the parse() method below.
118         // I don't know how to better avoid the code-duplication.
119         if (addressList != null && addressList.length() > 0) {
120             Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
121             for (int i = 0, length = tokens.length; i < length; ++i) {
122                 Rfc822Token token = tokens[i];
123                 String address = token.getAddress();
124                 if (!TextUtils.isEmpty(address) && !isValidAddress(address)) {
125                     return false;
126                 }
127             }
128         }
129         return true;
130     }
131 
132     /**
133      * Parse a comma-delimited list of addresses in RFC822 format and return an
134      * array of Address objects.
135      *
136      * @param addressList Address list in comma-delimited string.
137      * @return An array of 0 or more Addresses.
138      */
parse(String addressList)139     public static Address[] parse(String addressList) {
140         if (addressList == null || addressList.length() == 0) {
141             return EMPTY_ADDRESS_ARRAY;
142         }
143         Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
144         ArrayList<Address> addresses = new ArrayList<Address>();
145         for (int i = 0, length = tokens.length; i < length; ++i) {
146             Rfc822Token token = tokens[i];
147             String address = token.getAddress();
148             if (!TextUtils.isEmpty(address)) {
149                 if (isValidAddress(address)) {
150                     String name = token.getName();
151                     if (TextUtils.isEmpty(name)) {
152                         name = null;
153                     }
154                     addresses.add(new Address(address, name));
155                 }
156             }
157         }
158         return addresses.toArray(new Address[] {});
159     }
160 
161     /**
162      * Checks whether a string email address is valid.
163      * E.g. name@domain.com is valid.
164      */
165     @VisibleForTesting
isValidAddress(String address)166     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 
hashCode()195     public int hashCode() {
196         return getAddress().hashCode();
197     }
198 
199     /**
200      * Get human readable address string.
201      * Do not use this for email header.
202      *
203      * @return Human readable address string.  Not quoted and not encoded.
204      */
205     @Override
toString()206     public String toString() {
207         if (mPersonal != null && !mPersonal.equals(mAddress)) {
208             if (mPersonal.matches(".*[\\(\\)<>@,;:\\\\\".\\[\\]].*")) {
209                 return Utility.quoteString(mPersonal) + " <" + mAddress + ">";
210             } else {
211                 return mPersonal + " <" + mAddress + ">";
212             }
213         } else {
214             return mAddress;
215         }
216     }
217 
218     /**
219      * Get human readable comma-delimited address string.
220      *
221      * @param addresses Address array
222      * @return Human readable comma-delimited address string.
223      */
toString(Address[] addresses)224     public static String toString(Address[] addresses) {
225         return toString(addresses, ",");
226     }
227 
228     /**
229      * Get human readable address strings joined with the specified separator.
230      *
231      * @param addresses Address array
232      * @param separator Separator
233      * @return Human readable comma-delimited address string.
234      */
toString(Address[] addresses, String separator)235     public static String toString(Address[] addresses, String separator) {
236         if (addresses == null || addresses.length == 0) {
237             return null;
238         }
239         if (addresses.length == 1) {
240             return addresses[0].toString();
241         }
242         StringBuffer sb = new StringBuffer(addresses[0].toString());
243         for (int i = 1; i < addresses.length; i++) {
244             sb.append(separator);
245             // TODO: investigate why this .trim() is needed.
246             sb.append(addresses[i].toString().trim());
247         }
248         return sb.toString();
249     }
250 
251     /**
252      * Get RFC822/MIME compatible address string.
253      *
254      * @return RFC822/MIME compatible address string.
255      * It may be surrounded by double quote or quoted and MIME/base64 encoded if necessary.
256      */
toHeader()257     public String toHeader() {
258         if (mPersonal != null) {
259             return EncoderUtil.encodeAddressDisplayName(mPersonal) + " <" + mAddress + ">";
260         } else {
261             return mAddress;
262         }
263     }
264 
265     /**
266      * Get RFC822/MIME compatible comma-delimited address string.
267      *
268      * @param addresses Address array
269      * @return RFC822/MIME compatible comma-delimited address string.
270      * it may be surrounded by double quoted or quoted and MIME/base64 encoded if necessary.
271      */
toHeader(Address[] addresses)272     public static String toHeader(Address[] addresses) {
273         if (addresses == null || addresses.length == 0) {
274             return null;
275         }
276         if (addresses.length == 1) {
277             return addresses[0].toHeader();
278         }
279         StringBuffer sb = new StringBuffer(addresses[0].toHeader());
280         for (int i = 1; i < addresses.length; i++) {
281             // We need space character to be able to fold line.
282             sb.append(", ");
283             sb.append(addresses[i].toHeader());
284         }
285         return sb.toString();
286     }
287 
288     /**
289      * Get Human friendly address string.
290      *
291      * @return the personal part of this Address, or the address part if the
292      * personal part is not available
293      */
toFriendly()294     public String toFriendly() {
295         if (mPersonal != null && mPersonal.length() > 0) {
296             return mPersonal;
297         } else {
298             return mAddress;
299         }
300     }
301 
302     /**
303      * Creates a comma-delimited list of addresses in the "friendly" format (see toFriendly() for
304      * details on the per-address conversion).
305      *
306      * @param addresses Array of Address[] values
307      * @return A comma-delimited string listing all of the addresses supplied.  Null if source
308      * was null or empty.
309      */
toFriendly(Address[] addresses)310     public static String toFriendly(Address[] addresses) {
311         if (addresses == null || addresses.length == 0) {
312             return null;
313         }
314         if (addresses.length == 1) {
315             return addresses[0].toFriendly();
316         }
317         StringBuffer sb = new StringBuffer(addresses[0].toFriendly());
318         for (int i = 1; i < addresses.length; i++) {
319             sb.append(", ");
320             sb.append(addresses[i].toFriendly());
321         }
322         return sb.toString();
323     }
324 
325     /**
326      * Returns exactly the same result as Address.toString(Address.unpack(packedList)).
327      */
unpackToString(String packedList)328     public static String unpackToString(String packedList) {
329         return toString(unpack(packedList));
330     }
331 
332     /**
333      * Returns exactly the same result as Address.pack(Address.parse(textList)).
334      */
parseAndPack(String textList)335     public static String parseAndPack(String textList) {
336         return Address.pack(Address.parse(textList));
337     }
338 
339     /**
340      * Returns null if the packedList has 0 addresses, otherwise returns the first address.
341      * The same as Address.unpack(packedList)[0] for non-empty list.
342      * This is an utility method that offers some performance optimization opportunities.
343      */
unpackFirst(String packedList)344     public static Address unpackFirst(String packedList) {
345         Address[] array = unpack(packedList);
346         return array.length > 0 ? array[0] : null;
347     }
348 
349     /**
350      * Convert a packed list of addresses to a form suitable for use in an RFC822 header.
351      * This implementation is brute-force, and could be replaced with a more efficient version
352      * if desired.
353      */
packedToHeader(String packedList)354     public static String packedToHeader(String packedList) {
355         return toHeader(unpack(packedList));
356     }
357 
358     /**
359      * Unpacks an address list that is either CSV of RFC822 addresses OR (for backward
360      * compatibility) previously packed with pack()
361      * @param addressList string packed with pack() or CSV of RFC822 addresses
362      * @return array of addresses resulting from unpack
363      */
unpack(String addressList)364     public static Address[] unpack(String addressList) {
365         if (addressList == null || addressList.length() == 0) {
366             return EMPTY_ADDRESS_ARRAY;
367         }
368         // IF we're CSV, just parse
369         if ((addressList.indexOf(LIST_DELIMITER_PERSONAL) == -1) &&
370                 (addressList.indexOf(LIST_DELIMITER_EMAIL) == -1)) {
371             return Address.parse(addressList);
372         }
373         // Otherwise, do backward-compatibile unpack
374         ArrayList<Address> addresses = new ArrayList<Address>();
375         int length = addressList.length();
376         int pairStartIndex = 0;
377         int pairEndIndex = 0;
378 
379         /* addressEndIndex is only re-scanned (indexOf()) when a LIST_DELIMITER_PERSONAL
380            is used, not for every email address; i.e. not for every iteration of the while().
381            This reduces the theoretical complexity from quadratic to linear,
382            and provides some speed-up in practice by removing redundant scans of the string.
383         */
384         int addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL);
385 
386         while (pairStartIndex < length) {
387             pairEndIndex = addressList.indexOf(LIST_DELIMITER_EMAIL, pairStartIndex);
388             if (pairEndIndex == -1) {
389                 pairEndIndex = length;
390             }
391             Address address;
392             if (addressEndIndex == -1 || pairEndIndex <= addressEndIndex) {
393                 // in this case the DELIMITER_PERSONAL is in a future pair,
394                 // so don't use personal, and don't update addressEndIndex
395                 address = new Address(addressList.substring(pairStartIndex, pairEndIndex), null);
396             } else {
397                 address = new Address(addressList.substring(pairStartIndex, addressEndIndex),
398                                       addressList.substring(addressEndIndex + 1, pairEndIndex));
399                 // only update addressEndIndex when we use the LIST_DELIMITER_PERSONAL
400                 addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL, pairEndIndex + 1);
401             }
402             addresses.add(address);
403             pairStartIndex = pairEndIndex + 1;
404         }
405         return addresses.toArray(EMPTY_ADDRESS_ARRAY);
406     }
407 
408     /**
409      * Generate a String containing RFC822 addresses separated by commas
410      * NOTE: We used to "pack" these addresses in an app-specific format, but no longer do so
411      */
pack(Address[] addresses)412     public static String pack(Address[] addresses) {
413         return Address.toHeader(addresses);
414     }
415 
416     /**
417      * Produces the same result as pack(array), but only packs one (this) address.
418      */
pack()419     public String pack() {
420         final String address = getAddress();
421         final String personal = getPersonal();
422         if (personal == null) {
423             return address;
424         } else {
425             return address + LIST_DELIMITER_PERSONAL + personal;
426         }
427     }
428 }
429