Address.java revision aa27bc0e1c3bb6be4609b00007637a9d3e960f5e
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.google.common.annotations.VisibleForTesting;
24
25import org.apache.james.mime4j.codec.EncoderUtil;
26import org.apache.james.mime4j.decoder.DecoderUtil;
27
28import java.util.ArrayList;
29import 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 */
42public class Address {
43    /**
44     *  Address part, in the form local_part@domain_part. No surrounding angle brackets.
45     */
46    private 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    private 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
67    public Address(String address, String personal) {
68        setAddress(address);
69        setPersonal(personal);
70    }
71
72    public Address(String address) {
73        setAddress(address);
74    }
75
76    public String getAddress() {
77        return mAddress;
78    }
79
80    public void setAddress(String address) {
81        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     */
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     */
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        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     */
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     */
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     */
164    @VisibleForTesting
165    static boolean isValidAddress(String address) {
166        // Note: Some email provider may violate the standard, so here we only check that
167        // address consists of two part that are separated by '@', and domain part contains
168        // at least one '.'.
169        int len = address.length();
170        int firstAt = address.indexOf('@');
171        int lastAt = address.lastIndexOf('@');
172        int firstDot = address.indexOf('.', lastAt + 1);
173        int lastDot = address.lastIndexOf('.');
174        return firstAt > 0 && firstAt == lastAt && lastAt + 1 < firstDot
175            && firstDot <= lastDot && lastDot < len - 1;
176    }
177
178    @Override
179    public boolean equals(Object o) {
180        if (o instanceof Address) {
181            // It seems that the spec says that the "user" part is case-sensitive,
182            // while the domain part in case-insesitive.
183            // So foo@yahoo.com and Foo@yahoo.com are different.
184            // This may seem non-intuitive from the user POV, so we
185            // may re-consider it if it creates UI trouble.
186            // A problem case is "replyAll" sending to both
187            // a@b.c and to A@b.c, which turn out to be the same on the server.
188            // Leave unchanged for now (i.e. case-sensitive).
189            return getAddress().equals(((Address) o).getAddress());
190        }
191        return super.equals(o);
192    }
193
194    @Override
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
206    public String toString() {
207        if (mPersonal != null && !mPersonal.equals(mAddress)) {
208            if (mPersonal.matches(".*[\\(\\)<>@,;:\\\\\".\\[\\]].*")) {
209                return quoteString(mPersonal) + " <" + mAddress + ">";
210            } else {
211                return mPersonal + " <" + mAddress + ">";
212            }
213        } else {
214            return mAddress;
215        }
216    }
217
218    /**
219     * Ensures that the given string starts and ends with the double quote character. The string is
220     * not modified in any way except to add the double quote character to start and end if it's not
221     * already there.
222     *
223     * TODO: Rename this, because "quoteString()" can mean so many different things.
224     *
225     * sample -> "sample"
226     * "sample" -> "sample"
227     * ""sample"" -> "sample"
228     * "sample"" -> "sample"
229     * sa"mp"le -> "sa"mp"le"
230     * "sa"mp"le" -> "sa"mp"le"
231     * (empty string) -> ""
232     * " -> ""
233     */
234    public static String quoteString(String s) {
235        if (s == null) {
236            return null;
237        }
238        if (!s.matches("^\".*\"$")) {
239            return "\"" + s + "\"";
240        }
241        else {
242            return s;
243        }
244    }
245
246    /**
247     * Get human readable comma-delimited address string.
248     *
249     * @param addresses Address array
250     * @return Human readable comma-delimited address string.
251     */
252    public static String toString(Address[] addresses) {
253        return toString(addresses, ",");
254    }
255
256    /**
257     * Get human readable address strings joined with the specified separator.
258     *
259     * @param addresses Address array
260     * @param separator Separator
261     * @return Human readable comma-delimited address string.
262     */
263    public static String toString(Address[] addresses, String separator) {
264        if (addresses == null || addresses.length == 0) {
265            return null;
266        }
267        if (addresses.length == 1) {
268            return addresses[0].toString();
269        }
270        StringBuffer sb = new StringBuffer(addresses[0].toString());
271        for (int i = 1; i < addresses.length; i++) {
272            sb.append(separator);
273            // TODO: investigate why this .trim() is needed.
274            sb.append(addresses[i].toString().trim());
275        }
276        return sb.toString();
277    }
278
279    /**
280     * Get RFC822/MIME compatible address string.
281     *
282     * @return RFC822/MIME compatible address string.
283     * It may be surrounded by double quote or quoted and MIME/base64 encoded if necessary.
284     */
285    public String toHeader() {
286        if (mPersonal != null) {
287            return EncoderUtil.encodeAddressDisplayName(mPersonal) + " <" + mAddress + ">";
288        } else {
289            return mAddress;
290        }
291    }
292
293    /**
294     * Get RFC822/MIME compatible comma-delimited address string.
295     *
296     * @param addresses Address array
297     * @return RFC822/MIME compatible comma-delimited address string.
298     * it may be surrounded by double quoted or quoted and MIME/base64 encoded if necessary.
299     */
300    public static String toHeader(Address[] addresses) {
301        if (addresses == null || addresses.length == 0) {
302            return null;
303        }
304        if (addresses.length == 1) {
305            return addresses[0].toHeader();
306        }
307        StringBuffer sb = new StringBuffer(addresses[0].toHeader());
308        for (int i = 1; i < addresses.length; i++) {
309            // We need space character to be able to fold line.
310            sb.append(", ");
311            sb.append(addresses[i].toHeader());
312        }
313        return sb.toString();
314    }
315
316    /**
317     * Get Human friendly address string.
318     *
319     * @return the personal part of this Address, or the address part if the
320     * personal part is not available
321     */
322    public String toFriendly() {
323        if (mPersonal != null && mPersonal.length() > 0) {
324            return mPersonal;
325        } else {
326            return mAddress;
327        }
328    }
329
330    /**
331     * Creates a comma-delimited list of addresses in the "friendly" format (see toFriendly() for
332     * details on the per-address conversion).
333     *
334     * @param addresses Array of Address[] values
335     * @return A comma-delimited string listing all of the addresses supplied.  Null if source
336     * was null or empty.
337     */
338    public static String toFriendly(Address[] addresses) {
339        if (addresses == null || addresses.length == 0) {
340            return null;
341        }
342        if (addresses.length == 1) {
343            return addresses[0].toFriendly();
344        }
345        StringBuffer sb = new StringBuffer(addresses[0].toFriendly());
346        for (int i = 1; i < addresses.length; i++) {
347            sb.append(", ");
348            sb.append(addresses[i].toFriendly());
349        }
350        return sb.toString();
351    }
352
353    /**
354     * Returns exactly the same result as Address.toString(Address.unpack(packedList)).
355     */
356    public static String unpackToString(String packedList) {
357        return toString(unpack(packedList));
358    }
359
360    /**
361     * Returns exactly the same result as Address.pack(Address.parse(textList)).
362     */
363    public static String parseAndPack(String textList) {
364        return Address.pack(Address.parse(textList));
365    }
366
367    /**
368     * Returns null if the packedList has 0 addresses, otherwise returns the first address.
369     * The same as Address.unpack(packedList)[0] for non-empty list.
370     * This is an utility method that offers some performance optimization opportunities.
371     */
372    public static Address unpackFirst(String packedList) {
373        Address[] array = unpack(packedList);
374        return array.length > 0 ? array[0] : null;
375    }
376
377    /**
378     * Convert a packed list of addresses to a form suitable for use in an RFC822 header.
379     * This implementation is brute-force, and could be replaced with a more efficient version
380     * if desired.
381     */
382    public static String packedToHeader(String packedList) {
383        return toHeader(unpack(packedList));
384    }
385
386    /**
387     * Unpacks an address list that is either CSV of RFC822 addresses OR (for backward
388     * compatibility) previously packed with pack()
389     * @param addressList string packed with pack() or CSV of RFC822 addresses
390     * @return array of addresses resulting from unpack
391     */
392    public static Address[] unpack(String addressList) {
393        if (addressList == null || addressList.length() == 0) {
394            return EMPTY_ADDRESS_ARRAY;
395        }
396        // IF we're CSV, just parse
397        if ((addressList.indexOf(LIST_DELIMITER_PERSONAL) == -1) &&
398                (addressList.indexOf(LIST_DELIMITER_EMAIL) == -1)) {
399            return Address.parse(addressList);
400        }
401        // Otherwise, do backward-compatibile unpack
402        ArrayList<Address> addresses = new ArrayList<Address>();
403        int length = addressList.length();
404        int pairStartIndex = 0;
405        int pairEndIndex = 0;
406
407        /* addressEndIndex is only re-scanned (indexOf()) when a LIST_DELIMITER_PERSONAL
408           is used, not for every email address; i.e. not for every iteration of the while().
409           This reduces the theoretical complexity from quadratic to linear,
410           and provides some speed-up in practice by removing redundant scans of the string.
411        */
412        int addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL);
413
414        while (pairStartIndex < length) {
415            pairEndIndex = addressList.indexOf(LIST_DELIMITER_EMAIL, pairStartIndex);
416            if (pairEndIndex == -1) {
417                pairEndIndex = length;
418            }
419            Address address;
420            if (addressEndIndex == -1 || pairEndIndex <= addressEndIndex) {
421                // in this case the DELIMITER_PERSONAL is in a future pair,
422                // so don't use personal, and don't update addressEndIndex
423                address = new Address(addressList.substring(pairStartIndex, pairEndIndex), null);
424            } else {
425                address = new Address(addressList.substring(pairStartIndex, addressEndIndex),
426                                      addressList.substring(addressEndIndex + 1, pairEndIndex));
427                // only update addressEndIndex when we use the LIST_DELIMITER_PERSONAL
428                addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL, pairEndIndex + 1);
429            }
430            addresses.add(address);
431            pairStartIndex = pairEndIndex + 1;
432        }
433        return addresses.toArray(EMPTY_ADDRESS_ARRAY);
434    }
435
436    /**
437     * Generate a String containing RFC822 addresses separated by commas
438     * NOTE: We used to "pack" these addresses in an app-specific format, but no longer do so
439     */
440    public static String pack(Address[] addresses) {
441        return Address.toHeader(addresses);
442    }
443
444    /**
445     * Produces the same result as pack(array), but only packs one (this) address.
446     */
447    public String pack() {
448        final String address = getAddress();
449        final String personal = getPersonal();
450        if (personal == null) {
451            return address;
452        } else {
453            return address + LIST_DELIMITER_PERSONAL + personal;
454        }
455    }
456}
457