Address.java revision 0ba6b91842955635e1e927afe8836458af4aee4e
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
17package com.android.emailcommon.mail;
18
19import android.text.TextUtils;
20import android.text.util.Rfc822Token;
21import android.text.util.Rfc822Tokenizer;
22
23import com.android.emailcommon.utility.Utility;
24import com.google.common.annotations.VisibleForTesting;
25
26import org.apache.james.mime4j.codec.EncoderUtil;
27import org.apache.james.mime4j.decoder.DecoderUtil;
28
29import java.util.ArrayList;
30import 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 */
43public 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
68    public Address(String address, String personal) {
69        setAddress(address);
70        setPersonal(personal);
71    }
72
73    public Address(String address) {
74        setAddress(address);
75    }
76
77    public String getAddress() {
78        return mAddress;
79    }
80
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     */
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     */
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     */
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     */
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
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
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    public int hashCode() {
196        return toString().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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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