Address.java revision f24a125e5fedaae7bdc4eeceeb542cc5a7f3bdd9
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
62    // TODO: LOCAL_PART and DOMAIN_PART_PART are too permissive and can be improved.
63    // TODO: Fix this to better constrain comments.
64    /** Regex for the local part of an email address. */
65    private static final String LOCAL_PART = "[^@]+";
66    /** Regex for each part of the domain part, i.e. the thing between the dots. */
67    private static final String DOMAIN_PART_PART = "[[\\w][\\d]\\-\\(\\)\\[\\]]+";
68    /** Regex for the domain part, which is two or more {@link #DOMAIN_PART_PART} separated by . */
69    private static final String DOMAIN_PART =
70            "(" + DOMAIN_PART_PART + "\\.)+" + DOMAIN_PART_PART;
71
72    /** Pattern to check if an email address is valid. */
73    private static final Pattern EMAIL_ADDRESS =
74            Pattern.compile("\\A" + LOCAL_PART + "@" + DOMAIN_PART + "\\z");
75
76    private static final Address[] EMPTY_ADDRESS_ARRAY = new Address[0];
77
78    // delimiters are chars that do not appear in an email address, used by pack/unpack
79    private static final char LIST_DELIMITER_EMAIL = '\1';
80    private static final char LIST_DELIMITER_PERSONAL = '\2';
81
82    public Address(String address, String personal) {
83        setAddress(address);
84        setPersonal(personal);
85    }
86
87    public Address(String address) {
88        setAddress(address);
89    }
90
91    public String getAddress() {
92        return mAddress;
93    }
94
95    public void setAddress(String address) {
96        mAddress = REMOVE_OPTIONAL_BRACKET.matcher(address).replaceAll("$1");
97    }
98
99    /**
100     * Get name part as UTF-16 string. No surrounding double quote, and no MIME/base64 encoding.
101     *
102     * @return Name part of email address. Returns null if it is omitted.
103     */
104    public String getPersonal() {
105        return mPersonal;
106    }
107
108    /**
109     * Set name part from UTF-16 string. Optional surrounding double quote will be removed.
110     * It will be also unquoted and MIME/base64 decoded.
111     *
112     * @param personal name part of email address as UTF-16 string. Null is acceptable.
113     */
114    public void setPersonal(String personal) {
115        if (personal != null) {
116            personal = REMOVE_OPTIONAL_DQUOTE.matcher(personal).replaceAll("$1");
117            personal = UNQUOTE.matcher(personal).replaceAll("$1");
118            personal = DecoderUtil.decodeEncodedWords(personal);
119            if (personal.length() == 0) {
120                personal = null;
121            }
122        }
123        mPersonal = personal;
124    }
125
126    /**
127     * This method is used to check that all the addresses that the user
128     * entered in a list (e.g. To:) are valid, so that none is dropped.
129     */
130    public static boolean isAllValid(String addressList) {
131        // This code mimics the parse() method below.
132        // I don't know how to better avoid the code-duplication.
133        if (addressList != null && addressList.length() > 0) {
134            Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
135            for (int i = 0, length = tokens.length; i < length; ++i) {
136                Rfc822Token token = tokens[i];
137                String address = token.getAddress();
138                if (!TextUtils.isEmpty(address) && !isValidAddress(address)) {
139                    return false;
140                }
141            }
142        }
143        return true;
144    }
145
146    /**
147     * Parse a comma-delimited list of addresses in RFC822 format and return an
148     * array of Address objects.
149     *
150     * @param addressList Address list in comma-delimited string.
151     * @return An array of 0 or more Addresses.
152     */
153    public static Address[] parse(String addressList) {
154        if (addressList == null || addressList.length() == 0) {
155            return EMPTY_ADDRESS_ARRAY;
156        }
157        Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
158        ArrayList<Address> addresses = new ArrayList<Address>();
159        for (int i = 0, length = tokens.length; i < length; ++i) {
160            Rfc822Token token = tokens[i];
161            String address = token.getAddress();
162            if (!TextUtils.isEmpty(address)) {
163                if (isValidAddress(address)) {
164                    String name = token.getName();
165                    if (TextUtils.isEmpty(name)) {
166                        name = null;
167                    }
168                    addresses.add(new Address(address, name));
169                }
170            }
171        }
172        return addresses.toArray(new Address[] {});
173    }
174
175    /**
176     * Checks whether a string email address is valid.
177     * E.g. name@domain.com is valid.
178     */
179    @VisibleForTesting
180    static boolean isValidAddress(final String address) {
181        return EMAIL_ADDRESS.matcher(address).find();
182    }
183
184    @Override
185    public boolean equals(Object o) {
186        if (o instanceof Address) {
187            // It seems that the spec says that the "user" part is case-sensitive,
188            // while the domain part in case-insesitive.
189            // So foo@yahoo.com and Foo@yahoo.com are different.
190            // This may seem non-intuitive from the user POV, so we
191            // may re-consider it if it creates UI trouble.
192            // A problem case is "replyAll" sending to both
193            // a@b.c and to A@b.c, which turn out to be the same on the server.
194            // Leave unchanged for now (i.e. case-sensitive).
195            return getAddress().equals(((Address) o).getAddress());
196        }
197        return super.equals(o);
198    }
199
200    @Override
201    public int hashCode() {
202        return getAddress().hashCode();
203    }
204
205    /**
206     * Get human readable address string.
207     * Do not use this for email header.
208     *
209     * @return Human readable address string.  Not quoted and not encoded.
210     */
211    @Override
212    public String toString() {
213        if (mPersonal != null && !mPersonal.equals(mAddress)) {
214            if (mPersonal.matches(".*[\\(\\)<>@,;:\\\\\".\\[\\]].*")) {
215                return quoteString(mPersonal) + " <" + mAddress + ">";
216            } else {
217                return mPersonal + " <" + mAddress + ">";
218            }
219        } else {
220            return mAddress;
221        }
222    }
223
224    /**
225     * Ensures that the given string starts and ends with the double quote character. The string is
226     * not modified in any way except to add the double quote character to start and end if it's not
227     * already there.
228     *
229     * TODO: Rename this, because "quoteString()" can mean so many different things.
230     *
231     * sample -> "sample"
232     * "sample" -> "sample"
233     * ""sample"" -> "sample"
234     * "sample"" -> "sample"
235     * sa"mp"le -> "sa"mp"le"
236     * "sa"mp"le" -> "sa"mp"le"
237     * (empty string) -> ""
238     * " -> ""
239     */
240    public static String quoteString(String s) {
241        if (s == null) {
242            return null;
243        }
244        if (!s.matches("^\".*\"$")) {
245            return "\"" + s + "\"";
246        }
247        else {
248            return s;
249        }
250    }
251
252    /**
253     * Get human readable comma-delimited address string.
254     *
255     * @param addresses Address array
256     * @return Human readable comma-delimited address string.
257     */
258    public static String toString(Address[] addresses) {
259        return toString(addresses, ",");
260    }
261
262    /**
263     * Get human readable address strings joined with the specified separator.
264     *
265     * @param addresses Address array
266     * @param separator Separator
267     * @return Human readable comma-delimited address string.
268     */
269    public static String toString(Address[] addresses, String separator) {
270        if (addresses == null || addresses.length == 0) {
271            return null;
272        }
273        if (addresses.length == 1) {
274            return addresses[0].toString();
275        }
276        StringBuffer sb = new StringBuffer(addresses[0].toString());
277        for (int i = 1; i < addresses.length; i++) {
278            sb.append(separator);
279            // TODO: investigate why this .trim() is needed.
280            sb.append(addresses[i].toString().trim());
281        }
282        return sb.toString();
283    }
284
285    /**
286     * Get RFC822/MIME compatible address string.
287     *
288     * @return RFC822/MIME compatible address string.
289     * It may be surrounded by double quote or quoted and MIME/base64 encoded if necessary.
290     */
291    public String toHeader() {
292        if (mPersonal != null) {
293            return EncoderUtil.encodeAddressDisplayName(mPersonal) + " <" + mAddress + ">";
294        } else {
295            return mAddress;
296        }
297    }
298
299    /**
300     * Get RFC822/MIME compatible comma-delimited address string.
301     *
302     * @param addresses Address array
303     * @return RFC822/MIME compatible comma-delimited address string.
304     * it may be surrounded by double quoted or quoted and MIME/base64 encoded if necessary.
305     */
306    public static String toHeader(Address[] addresses) {
307        if (addresses == null || addresses.length == 0) {
308            return null;
309        }
310        if (addresses.length == 1) {
311            return addresses[0].toHeader();
312        }
313        StringBuffer sb = new StringBuffer(addresses[0].toHeader());
314        for (int i = 1; i < addresses.length; i++) {
315            // We need space character to be able to fold line.
316            sb.append(", ");
317            sb.append(addresses[i].toHeader());
318        }
319        return sb.toString();
320    }
321
322    /**
323     * Get Human friendly address string.
324     *
325     * @return the personal part of this Address, or the address part if the
326     * personal part is not available
327     */
328    public String toFriendly() {
329        if (mPersonal != null && mPersonal.length() > 0) {
330            return mPersonal;
331        } else {
332            return mAddress;
333        }
334    }
335
336    /**
337     * Creates a comma-delimited list of addresses in the "friendly" format (see toFriendly() for
338     * details on the per-address conversion).
339     *
340     * @param addresses Array of Address[] values
341     * @return A comma-delimited string listing all of the addresses supplied.  Null if source
342     * was null or empty.
343     */
344    public static String toFriendly(Address[] addresses) {
345        if (addresses == null || addresses.length == 0) {
346            return null;
347        }
348        if (addresses.length == 1) {
349            return addresses[0].toFriendly();
350        }
351        StringBuffer sb = new StringBuffer(addresses[0].toFriendly());
352        for (int i = 1; i < addresses.length; i++) {
353            sb.append(", ");
354            sb.append(addresses[i].toFriendly());
355        }
356        return sb.toString();
357    }
358
359    /**
360     * Returns exactly the same result as Address.toString(Address.unpack(packedList)).
361     */
362    public static String unpackToString(String packedList) {
363        return toString(unpack(packedList));
364    }
365
366    /**
367     * Returns exactly the same result as Address.pack(Address.parse(textList)).
368     */
369    public static String parseAndPack(String textList) {
370        return Address.pack(Address.parse(textList));
371    }
372
373    /**
374     * Returns null if the packedList has 0 addresses, otherwise returns the first address.
375     * The same as Address.unpack(packedList)[0] for non-empty list.
376     * This is an utility method that offers some performance optimization opportunities.
377     */
378    public static Address unpackFirst(String packedList) {
379        Address[] array = unpack(packedList);
380        return array.length > 0 ? array[0] : null;
381    }
382
383    /**
384     * Convert a packed list of addresses to a form suitable for use in an RFC822 header.
385     * This implementation is brute-force, and could be replaced with a more efficient version
386     * if desired.
387     */
388    public static String packedToHeader(String packedList) {
389        return toHeader(unpack(packedList));
390    }
391
392    /**
393     * Unpacks an address list that is either CSV of RFC822 addresses OR (for backward
394     * compatibility) previously packed with pack()
395     * @param addressList string packed with pack() or CSV of RFC822 addresses
396     * @return array of addresses resulting from unpack
397     */
398    public static Address[] unpack(String addressList) {
399        if (addressList == null || addressList.length() == 0) {
400            return EMPTY_ADDRESS_ARRAY;
401        }
402        // IF we're CSV, just parse
403        if ((addressList.indexOf(LIST_DELIMITER_PERSONAL) == -1) &&
404                (addressList.indexOf(LIST_DELIMITER_EMAIL) == -1)) {
405            return Address.parse(addressList);
406        }
407        // Otherwise, do backward-compatibile unpack
408        ArrayList<Address> addresses = new ArrayList<Address>();
409        int length = addressList.length();
410        int pairStartIndex = 0;
411        int pairEndIndex = 0;
412
413        /* addressEndIndex is only re-scanned (indexOf()) when a LIST_DELIMITER_PERSONAL
414           is used, not for every email address; i.e. not for every iteration of the while().
415           This reduces the theoretical complexity from quadratic to linear,
416           and provides some speed-up in practice by removing redundant scans of the string.
417        */
418        int addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL);
419
420        while (pairStartIndex < length) {
421            pairEndIndex = addressList.indexOf(LIST_DELIMITER_EMAIL, pairStartIndex);
422            if (pairEndIndex == -1) {
423                pairEndIndex = length;
424            }
425            Address address;
426            if (addressEndIndex == -1 || pairEndIndex <= addressEndIndex) {
427                // in this case the DELIMITER_PERSONAL is in a future pair,
428                // so don't use personal, and don't update addressEndIndex
429                address = new Address(addressList.substring(pairStartIndex, pairEndIndex), null);
430            } else {
431                address = new Address(addressList.substring(pairStartIndex, addressEndIndex),
432                                      addressList.substring(addressEndIndex + 1, pairEndIndex));
433                // only update addressEndIndex when we use the LIST_DELIMITER_PERSONAL
434                addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL, pairEndIndex + 1);
435            }
436            addresses.add(address);
437            pairStartIndex = pairEndIndex + 1;
438        }
439        return addresses.toArray(EMPTY_ADDRESS_ARRAY);
440    }
441
442    /**
443     * Generate a String containing RFC822 addresses separated by commas
444     * NOTE: We used to "pack" these addresses in an app-specific format, but no longer do so
445     */
446    public static String pack(Address[] addresses) {
447        return Address.toHeader(addresses);
448    }
449
450    /**
451     * Produces the same result as pack(array), but only packs one (this) address.
452     */
453    public String pack() {
454        final String address = getAddress();
455        final String personal = getPersonal();
456        if (personal == null) {
457            return address;
458        } else {
459            return address + LIST_DELIMITER_PERSONAL + personal;
460        }
461    }
462}
463