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