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