Address.java revision 821e578a71c7015646522e729600618f0ec16fc0
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 */
16package com.android.emailcommon.mail;
17
18import android.os.Parcel;
19import android.os.Parcelable;
20import android.text.Html;
21import android.text.TextUtils;
22import android.text.util.Rfc822Token;
23import android.text.util.Rfc822Tokenizer;
24
25import com.android.mail.utils.LogTag;
26import com.android.mail.utils.LogUtils;
27import com.google.common.annotations.VisibleForTesting;
28
29import org.apache.james.mime4j.codec.EncoderUtil;
30import org.apache.james.mime4j.decoder.DecoderUtil;
31
32import java.util.ArrayList;
33import java.util.regex.Pattern;
34
35/**
36 * This class represent email address.
37 *
38 * RFC822 email address may have following format.
39 *   "name" <address> (comment)
40 *   "name" <address>
41 *   name <address>
42 *   address
43 * Name and comment part should be MIME/base64 encoded in header if necessary.
44 *
45 */
46public class Address implements Parcelable {
47    public static final String ADDRESS_DELIMETER = ",";
48    /**
49     *  Address part, in the form local_part@domain_part. No surrounding angle brackets.
50     */
51    private String mAddress;
52
53    /**
54     * Name part. No surrounding double quote, and no MIME/base64 encoding.
55     * This must be null if Address has no name part.
56     */
57    private String mPersonal;
58
59    /**
60     * When personal is set, it will return the first token of the personal
61     * string. Otherwise, it will return the e-mail address up to the '@' sign.
62     */
63    private String mSimplifiedName;
64
65    // Regex that matches address surrounded by '<>' optionally. '^<?([^>]+)>?$'
66    private static final Pattern REMOVE_OPTIONAL_BRACKET = Pattern.compile("^<?([^>]+)>?$");
67    // Regex that matches personal name surrounded by '""' optionally. '^"?([^"]+)"?$'
68    private static final Pattern REMOVE_OPTIONAL_DQUOTE = Pattern.compile("^\"?([^\"]*)\"?$");
69    // Regex that matches escaped character '\\([\\"])'
70    private static final Pattern UNQUOTE = Pattern.compile("\\\\([\\\\\"])");
71
72    // TODO: LOCAL_PART and DOMAIN_PART_PART are too permissive and can be improved.
73    // TODO: Fix this to better constrain comments.
74    /** Regex for the local part of an email address. */
75    private static final String LOCAL_PART = "[^@]+";
76    /** Regex for each part of the domain part, i.e. the thing between the dots. */
77    private static final String DOMAIN_PART_PART = "[[\\w][\\d]\\-\\(\\)\\[\\]]+";
78    /** Regex for the domain part, which is two or more {@link #DOMAIN_PART_PART} separated by . */
79    private static final String DOMAIN_PART =
80            "(" + DOMAIN_PART_PART + "\\.)+" + DOMAIN_PART_PART;
81
82    /** Pattern to check if an email address is valid. */
83    private static final Pattern EMAIL_ADDRESS =
84            Pattern.compile("\\A" + LOCAL_PART + "@" + DOMAIN_PART + "\\z");
85
86    private static final Address[] EMPTY_ADDRESS_ARRAY = new Address[0];
87
88    // delimiters are chars that do not appear in an email address, used by pack/unpack
89    private static final char LIST_DELIMITER_EMAIL = '\1';
90    private static final char LIST_DELIMITER_PERSONAL = '\2';
91
92    private static final String LOG_TAG = LogTag.getLogTag();
93
94    public Address(String address) {
95        setAddress(address);
96    }
97
98    public Address(String address, String personal) {
99        setPersonal(personal);
100        setAddress(address);
101    }
102
103    /**
104     * Returns a simplified string for this e-mail address.
105     * When a name is known, it will return the first token of that name. Otherwise, it will
106     * return the e-mail address up to the '@' sign.
107     */
108    public String getSimplifiedName() {
109        if (mSimplifiedName == null) {
110            if (TextUtils.isEmpty(mPersonal) && !TextUtils.isEmpty(mAddress)) {
111                int atSign = mAddress.indexOf('@');
112                mSimplifiedName = (atSign != -1) ? mAddress.substring(0, atSign) : "";
113            } else if (!TextUtils.isEmpty(mPersonal)) {
114
115                // TODO: use Contacts' NameSplitter for more reliable first-name extraction
116
117                int end = mPersonal.indexOf(' ');
118                while (end > 0 && mPersonal.charAt(end - 1) == ',') {
119                    end--;
120                }
121                mSimplifiedName = (end < 1) ? mPersonal : mPersonal.substring(0, end);
122
123            } else {
124                LogUtils.w(LOG_TAG, "Unable to get a simplified name");
125                mSimplifiedName = "";
126            }
127        }
128        return mSimplifiedName;
129    }
130
131    public static synchronized Address getEmailAddress(String rawAddress) {
132        if (TextUtils.isEmpty(rawAddress)) {
133            return null;
134        }
135        String name, address;
136        final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(rawAddress);
137        if (tokens.length > 0) {
138            final String tokenizedName = tokens[0].getName();
139            name = tokenizedName != null ? Html.fromHtml(tokenizedName.trim()).toString()
140                    : "";
141            address = Html.fromHtml(tokens[0].getAddress()).toString();
142        } else {
143            name = "";
144            address = rawAddress == null ?
145                    "" : Html.fromHtml(rawAddress).toString();
146        }
147        return new Address(address, name);
148    }
149
150    public String getAddress() {
151        return mAddress;
152    }
153
154    public void setAddress(String address) {
155        mAddress = REMOVE_OPTIONAL_BRACKET.matcher(address).replaceAll("$1");
156    }
157
158    /**
159     * Get name part as UTF-16 string. No surrounding double quote, and no MIME/base64 encoding.
160     *
161     * @return Name part of email address. Returns null if it is omitted.
162     */
163    public String getPersonal() {
164        return mPersonal;
165    }
166
167    /**
168     * Set personal part from UTF-16 string. Optional surrounding double quote will be removed.
169     * It will be also unquoted and MIME/base64 decoded.
170     *
171     * @param personal name part of email address as UTF-16 string. Null is acceptable.
172     */
173    public void setPersonal(String personal) {
174        mPersonal = decodeAddressPersonal(personal);
175    }
176
177    /**
178     * Decodes name from UTF-16 string. Optional surrounding double quote will be removed.
179     * It will be also unquoted and MIME/base64 decoded.
180     *
181     * @param personal name part of email address as UTF-16 string. Null is acceptable.
182     */
183    public static String decodeAddressPersonal(String personal) {
184        if (personal != null) {
185            personal = REMOVE_OPTIONAL_DQUOTE.matcher(personal).replaceAll("$1");
186            personal = UNQUOTE.matcher(personal).replaceAll("$1");
187            personal = DecoderUtil.decodeEncodedWords(personal);
188            if (personal.length() == 0) {
189                personal = null;
190            }
191        }
192        return personal;
193    }
194
195    /**
196     * This method is used to check that all the addresses that the user
197     * entered in a list (e.g. To:) are valid, so that none is dropped.
198     */
199    public static boolean isAllValid(String addressList) {
200        // This code mimics the parse() method below.
201        // I don't know how to better avoid the code-duplication.
202        if (addressList != null && addressList.length() > 0) {
203            Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
204            for (int i = 0, length = tokens.length; i < length; ++i) {
205                Rfc822Token token = tokens[i];
206                String address = token.getAddress();
207                if (!TextUtils.isEmpty(address) && !isValidAddress(address)) {
208                    return false;
209                }
210            }
211        }
212        return true;
213    }
214
215    /**
216     * Parse a comma-delimited list of addresses in RFC822 format and return an
217     * array of Address objects.
218     *
219     * @param addressList Address list in comma-delimited string.
220     * @return An array of 0 or more Addresses.
221     */
222    public static Address[] parse(String addressList) {
223        if (addressList == null || addressList.length() == 0) {
224            return EMPTY_ADDRESS_ARRAY;
225        }
226        Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
227        ArrayList<Address> addresses = new ArrayList<Address>();
228        for (int i = 0, length = tokens.length; i < length; ++i) {
229            Rfc822Token token = tokens[i];
230            String address = token.getAddress();
231            if (!TextUtils.isEmpty(address)) {
232                if (isValidAddress(address)) {
233                    String name = token.getName();
234                    if (TextUtils.isEmpty(name)) {
235                        name = null;
236                    }
237                    addresses.add(new Address(address, name));
238                }
239            }
240        }
241        return addresses.toArray(new Address[addresses.size()]);
242    }
243
244    /**
245     * Checks whether a string email address is valid.
246     * E.g. name@domain.com is valid.
247     */
248    @VisibleForTesting
249    static boolean isValidAddress(final String address) {
250        return EMAIL_ADDRESS.matcher(address).find();
251    }
252
253    @Override
254    public boolean equals(Object o) {
255        if (o instanceof Address) {
256            // It seems that the spec says that the "user" part is case-sensitive,
257            // while the domain part in case-insesitive.
258            // So foo@yahoo.com and Foo@yahoo.com are different.
259            // This may seem non-intuitive from the user POV, so we
260            // may re-consider it if it creates UI trouble.
261            // A problem case is "replyAll" sending to both
262            // a@b.c and to A@b.c, which turn out to be the same on the server.
263            // Leave unchanged for now (i.e. case-sensitive).
264            return getAddress().equals(((Address) o).getAddress());
265        }
266        return super.equals(o);
267    }
268
269    @Override
270    public int hashCode() {
271        return getAddress().hashCode();
272    }
273
274    /**
275     * Get human readable address string.
276     * Do not use this for email header.
277     *
278     * @return Human readable address string.  Not quoted and not encoded.
279     */
280    @Override
281    public String toString() {
282        if (mPersonal != null && !mPersonal.equals(mAddress)) {
283            if (mPersonal.matches(".*[\\(\\)<>@,;:\\\\\".\\[\\]].*")) {
284                return ensureQuotedString(mPersonal) + " <" + mAddress + ">";
285            } else {
286                return mPersonal + " <" + mAddress + ">";
287            }
288        } else {
289            return mAddress;
290        }
291    }
292
293    /**
294     * Ensures that the given string starts and ends with the double quote character. The string is
295     * not modified in any way except to add the double quote character to start and end if it's not
296     * already there.
297     *
298     * sample -> "sample"
299     * "sample" -> "sample"
300     * ""sample"" -> "sample"
301     * "sample"" -> "sample"
302     * sa"mp"le -> "sa"mp"le"
303     * "sa"mp"le" -> "sa"mp"le"
304     * (empty string) -> ""
305     * " -> ""
306     */
307    private static String ensureQuotedString(String s) {
308        if (s == null) {
309            return null;
310        }
311        if (!s.matches("^\".*\"$")) {
312            return "\"" + s + "\"";
313        } else {
314            return s;
315        }
316    }
317
318    /**
319     * Get human readable comma-delimited address string.
320     *
321     * @param addresses Address array
322     * @return Human readable comma-delimited address string.
323     */
324    public static String toString(Address[] addresses) {
325        return toString(addresses, ADDRESS_DELIMETER);
326    }
327
328    /**
329     * Get human readable address strings joined with the specified separator.
330     *
331     * @param addresses Address array
332     * @param separator Separator
333     * @return Human readable comma-delimited address string.
334     */
335    public static String toString(Address[] addresses, String separator) {
336        if (addresses == null || addresses.length == 0) {
337            return null;
338        }
339        if (addresses.length == 1) {
340            return addresses[0].toString();
341        }
342        StringBuilder sb = new StringBuilder(addresses[0].toString());
343        for (int i = 1; i < addresses.length; i++) {
344            sb.append(separator);
345            // TODO: investigate why this .trim() is needed.
346            sb.append(addresses[i].toString().trim());
347        }
348        return sb.toString();
349    }
350
351    /**
352     * Get RFC822/MIME compatible address string.
353     *
354     * @return RFC822/MIME compatible address string.
355     * It may be surrounded by double quote or quoted and MIME/base64 encoded if necessary.
356     */
357    public String toHeader() {
358        if (mPersonal != null) {
359            return EncoderUtil.encodeAddressDisplayName(mPersonal) + " <" + mAddress + ">";
360        } else {
361            return mAddress;
362        }
363    }
364
365    /**
366     * Get RFC822/MIME compatible comma-delimited address string.
367     *
368     * @param addresses Address array
369     * @return RFC822/MIME compatible comma-delimited address string.
370     * it may be surrounded by double quoted or quoted and MIME/base64 encoded if necessary.
371     */
372    public static String toHeader(Address[] addresses) {
373        if (addresses == null || addresses.length == 0) {
374            return null;
375        }
376        if (addresses.length == 1) {
377            return addresses[0].toHeader();
378        }
379        StringBuilder sb = new StringBuilder(addresses[0].toHeader());
380        for (int i = 1; i < addresses.length; i++) {
381            // We need space character to be able to fold line.
382            sb.append(", ");
383            sb.append(addresses[i].toHeader());
384        }
385        return sb.toString();
386    }
387
388    /**
389     * Get Human friendly address string.
390     *
391     * @return the personal part of this Address, or the address part if the
392     * personal part is not available
393     */
394    public String toFriendly() {
395        if (mPersonal != null && mPersonal.length() > 0) {
396            return mPersonal;
397        } else {
398            return mAddress;
399        }
400    }
401
402    /**
403     * Creates a comma-delimited list of addresses in the "friendly" format (see toFriendly() for
404     * details on the per-address conversion).
405     *
406     * @param addresses Array of Address[] values
407     * @return A comma-delimited string listing all of the addresses supplied.  Null if source
408     * was null or empty.
409     */
410    public static String toFriendly(Address[] addresses) {
411        if (addresses == null || addresses.length == 0) {
412            return null;
413        }
414        if (addresses.length == 1) {
415            return addresses[0].toFriendly();
416        }
417        StringBuilder sb = new StringBuilder(addresses[0].toFriendly());
418        for (int i = 1; i < addresses.length; i++) {
419            sb.append(", ");
420            sb.append(addresses[i].toFriendly());
421        }
422        return sb.toString();
423    }
424
425    /**
426     * Returns exactly the same result as Address.toString(Address.unpack(packedList)).
427     */
428    public static String unpackToString(String packedList) {
429        return toString(unpack(packedList));
430    }
431
432    /**
433     * Returns exactly the same result as Address.pack(Address.parse(textList)).
434     */
435    public static String parseAndPack(String textList) {
436        return Address.pack(Address.parse(textList));
437    }
438
439    /**
440     * Returns null if the packedList has 0 addresses, otherwise returns the first address.
441     * The same as Address.unpack(packedList)[0] for non-empty list.
442     * This is an utility method that offers some performance optimization opportunities.
443     */
444    public static Address unpackFirst(String packedList) {
445        Address[] array = unpack(packedList);
446        return array.length > 0 ? array[0] : null;
447    }
448
449    /**
450     * Convert a packed list of addresses to a form suitable for use in an RFC822 header.
451     * This implementation is brute-force, and could be replaced with a more efficient version
452     * if desired.
453     */
454    public static String packedToHeader(String packedList) {
455        return toHeader(unpack(packedList));
456    }
457
458    /**
459     * Unpacks an address list that is either CSV of RFC822 addresses OR (for backward
460     * compatibility) previously packed with pack()
461     * @param addressList string packed with pack() or CSV of RFC822 addresses
462     * @return array of addresses resulting from unpack
463     */
464    public static Address[] unpack(String addressList) {
465        if (addressList == null || addressList.length() == 0) {
466            return EMPTY_ADDRESS_ARRAY;
467        }
468        // IF we're CSV, just parse
469        if ((addressList.indexOf(LIST_DELIMITER_PERSONAL) == -1) &&
470                (addressList.indexOf(LIST_DELIMITER_EMAIL) == -1)) {
471            return Address.parse(addressList);
472        }
473        // Otherwise, do backward-compatible unpack
474        ArrayList<Address> addresses = new ArrayList<Address>();
475        int length = addressList.length();
476        int pairStartIndex = 0;
477        int pairEndIndex;
478
479        /* addressEndIndex is only re-scanned (indexOf()) when a LIST_DELIMITER_PERSONAL
480           is used, not for every email address; i.e. not for every iteration of the while().
481           This reduces the theoretical complexity from quadratic to linear,
482           and provides some speed-up in practice by removing redundant scans of the string.
483        */
484        int addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL);
485
486        while (pairStartIndex < length) {
487            pairEndIndex = addressList.indexOf(LIST_DELIMITER_EMAIL, pairStartIndex);
488            if (pairEndIndex == -1) {
489                pairEndIndex = length;
490            }
491            Address address;
492            if (addressEndIndex == -1 || pairEndIndex <= addressEndIndex) {
493                // in this case the DELIMITER_PERSONAL is in a future pair,
494                // so don't use personal, and don't update addressEndIndex
495                address = new Address(addressList.substring(pairStartIndex, pairEndIndex), null);
496            } else {
497                address = new Address(addressList.substring(pairStartIndex, addressEndIndex),
498                        addressList.substring(addressEndIndex + 1, pairEndIndex));
499                // only update addressEndIndex when we use the LIST_DELIMITER_PERSONAL
500                addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL, pairEndIndex + 1);
501            }
502            addresses.add(address);
503            pairStartIndex = pairEndIndex + 1;
504        }
505        return addresses.toArray(new Address[addresses.size()]);
506    }
507
508    /**
509     * Generate a String containing RFC822 addresses separated by commas
510     * NOTE: We used to "pack" these addresses in an app-specific format, but no longer do so
511     */
512    public static String pack(Address[] addresses) {
513        return Address.toHeader(addresses);
514    }
515
516    /**
517     * Produces the same result as pack(array), but only packs one (this) address.
518     */
519    public String pack() {
520        return toHeader();
521    }
522
523    public static final Creator<Address> CREATOR = new Creator<Address>() {
524        @Override
525        public Address createFromParcel(Parcel parcel) {
526            return new Address(parcel);
527        }
528
529        @Override
530        public Address[] newArray(int size) {
531            return new Address[size];
532        }
533    };
534
535    public Address(Parcel in) {
536        setPersonal(in.readString());
537        setAddress(in.readString());
538    }
539
540    @Override
541    public int describeContents() {
542        return 0;
543    }
544
545    @Override
546    public void writeToParcel(Parcel out, int flags) {
547        out.writeString(mPersonal);
548        out.writeString(mAddress);
549    }
550}
551