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.common;
18
19import android.text.TextUtils;
20import android.text.util.Rfc822Token;
21import android.text.util.Rfc822Tokenizer;
22import android.util.Patterns;
23import android.widget.AutoCompleteTextView;
24
25import java.util.regex.Pattern;
26
27/**
28 * This class works as a Validator for AutoCompleteTextView for
29 * email addresses.  If a token does not appear to be a valid address,
30 * it is trimmed of characters that cannot legitimately appear in one
31 * and has the specified domain name added.  It is meant for use with
32 * {@link Rfc822Token} and {@link Rfc822Tokenizer}.
33 *
34 * @deprecated In the future make sure we don't quietly alter the user's
35 *             text in ways they did not intend.  Meanwhile, hide this
36 *             class from the public API because it does not even have
37 *             a full understanding of the syntax it claims to correct.
38 * @hide
39 */
40@Deprecated
41public class Rfc822Validator implements AutoCompleteTextView.Validator {
42    /**
43     * Expression that matches the local part of an email address.
44     * This expression does not follow the constraints of the RFC towards the dots, because the
45     * de facto standard is to allow them anywhere.
46     *
47     * It is however a simplification and it will not validate the double-quote syntax.
48     */
49    private static final String EMAIL_ADDRESS_LOCALPART_REGEXP =
50        "((?!\\s)[\\.\\w!#$%&'*+\\-/=?^`{|}~\u0080-\uFFFE])+";
51
52    /**
53     * Alias of characters that can be used in IRI, as per RFC 3987.
54     */
55    private static final String GOOD_IRI_CHAR = Patterns.GOOD_IRI_CHAR;
56
57    /**
58     * Regular expression for a domain label, as per RFC 3490.
59     * Its total length must not exceed 63 octets, according to RFC 5890.
60     */
61    private static final String LABEL_REGEXP =
62        "([" + GOOD_IRI_CHAR + "][" + GOOD_IRI_CHAR + "\\-]{0,61})?[" + GOOD_IRI_CHAR + "]";
63
64    /**
65     * Expression that matches a domain name, including international domain names in Punycode or
66     * Unicode.
67     */
68    private static final String DOMAIN_REGEXP =
69        "("+ LABEL_REGEXP + "\\.)+"                 // Subdomains and domain
70        // Top-level domain must be at least 2 chars
71        + "[" + GOOD_IRI_CHAR + "][" + GOOD_IRI_CHAR + "\\-]{0,61}[" + GOOD_IRI_CHAR + "]";
72
73    /**
74     * Pattern for an email address.
75     *
76     * It is similar to {@link android.util.Patterns#EMAIL_ADDRESS}, but also accepts Unicode
77     * characters.
78     */
79    private static final Pattern EMAIL_ADDRESS_PATTERN =
80            Pattern.compile(EMAIL_ADDRESS_LOCALPART_REGEXP + "@" + DOMAIN_REGEXP);
81
82    private String mDomain;
83    private boolean mRemoveInvalid = false;
84
85    /**
86     * Constructs a new validator that uses the specified domain name as
87     * the default when none is specified.
88     */
89    public Rfc822Validator(String domain) {
90        mDomain = domain;
91    }
92
93    /**
94     * {@inheritDoc}
95     */
96    public boolean isValid(CharSequence text) {
97        Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(text);
98        return tokens.length == 1 &&
99               EMAIL_ADDRESS_PATTERN.
100                   matcher(tokens[0].getAddress()).matches();
101    }
102
103    /**
104     * Specify if the validator should remove invalid tokens instead of trying
105     * to fix them. This can be used to strip results of incorrectly formatted
106     * tokens.
107     *
108     * @param remove true to remove tokens with the wrong format, false to
109     *            attempt to fix them
110     */
111    public void setRemoveInvalid(boolean remove) {
112        mRemoveInvalid = remove;
113    }
114
115    /**
116     * @return a string in which all the characters that are illegal for the username
117     * or the domain name part of the email address have been removed.
118     */
119    private String removeIllegalCharacters(String s) {
120        StringBuilder result = new StringBuilder();
121        int length = s.length();
122        for (int i = 0; i < length; i++) {
123            char c = s.charAt(i);
124
125            /*
126             * An RFC822 atom can contain any ASCII printing character
127             * except for periods and any of the following punctuation.
128             * A local-part can contain multiple atoms, concatenated by
129             * periods, so do allow periods here.
130             */
131
132            if (c <= ' ' || c > '~') {
133                continue;
134            }
135
136            if (c == '(' || c == ')' || c == '<' || c == '>' ||
137                c == '@' || c == ',' || c == ';' || c == ':' ||
138                c == '\\' || c == '"' || c == '[' || c == ']') {
139                continue;
140            }
141
142            result.append(c);
143        }
144        return result.toString();
145    }
146
147    /**
148     * {@inheritDoc}
149     */
150    public CharSequence fixText(CharSequence cs) {
151        // Return an empty string if the email address only contains spaces, \n or \t
152        if (TextUtils.getTrimmedLength(cs) == 0) return "";
153
154        Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(cs);
155        StringBuilder sb = new StringBuilder();
156
157        for (int i = 0; i < tokens.length; i++) {
158            String text = tokens[i].getAddress();
159
160            if (mRemoveInvalid && !isValid(text)) {
161                continue;
162            }
163            int index = text.indexOf('@');
164            if (index < 0) {
165                // append the domain of the account if it exists
166                if (mDomain != null) {
167                    tokens[i].setAddress(removeIllegalCharacters(text) + "@" + mDomain);
168                }
169            } else {
170                // Otherwise, remove the illegal characters on both sides of the '@'
171                String fix = removeIllegalCharacters(text.substring(0, index));
172                if (TextUtils.isEmpty(fix)) {
173                    // if the address is empty after removing invalid chars
174                    // don't use it
175                    continue;
176                }
177                String domain = removeIllegalCharacters(text.substring(index + 1));
178                boolean emptyDomain = domain.length() == 0;
179                if (!emptyDomain || mDomain != null) {
180                    tokens[i].setAddress(fix + "@" + (!emptyDomain ? domain : mDomain));
181                }
182            }
183
184            sb.append(tokens[i].toString());
185            if (i + 1 < tokens.length) {
186                sb.append(", ");
187            }
188        }
189
190        return sb;
191    }
192}
193