1 /*
2  * Copyright (C) 2009 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 
17 package com.android.email.activity.setup;
18 
19 import android.content.ContentResolver;
20 import android.content.ContentValues;
21 import android.content.Context;
22 import android.content.res.XmlResourceParser;
23 import android.net.Uri;
24 import android.text.TextUtils;
25 
26 import com.android.email.R;
27 import com.android.email.provider.AccountBackupRestore;
28 import com.android.emailcommon.Logging;
29 import com.android.emailcommon.VendorPolicyLoader;
30 import com.android.emailcommon.VendorPolicyLoader.OAuthProvider;
31 import com.android.emailcommon.VendorPolicyLoader.Provider;
32 import com.android.emailcommon.provider.Account;
33 import com.android.emailcommon.provider.EmailContent.AccountColumns;
34 import com.android.emailcommon.provider.QuickResponse;
35 import com.android.emailcommon.service.PolicyServiceProxy;
36 import com.android.emailcommon.utility.Utility;
37 import com.android.mail.utils.LogUtils;
38 import com.google.common.annotations.VisibleForTesting;
39 
40 import java.util.ArrayList;
41 import java.util.List;
42 
43 public class AccountSettingsUtils {
44 
45     /** Pattern to match any part of a domain */
46     private final static String WILD_STRING = "*";
47     /** Will match any, single character */
48     private final static char WILD_CHARACTER = '?';
49     private final static String DOMAIN_SEPARATOR = "\\.";
50 
51     /**
52      * Commits the UI-related settings of an account to the provider.  This is static so that it
53      * can be used by the various account activities.  If the account has never been saved, this
54      * method saves it; otherwise, it just saves the settings.
55      * @param context the context of the caller
56      * @param account the account whose settings will be committed
57      */
commitSettings(Context context, Account account)58     public static void commitSettings(Context context, Account account) {
59         if (!account.isSaved()) {
60             account.save(context);
61 
62             if (account.mPolicy != null) {
63                 // TODO: we need better handling for unsupported policies
64                 // For now, just clear the unsupported policies, as the server will (hopefully)
65                 // just reject our sync attempts if it's not happy with half-measures
66                 if (account.mPolicy.mProtocolPoliciesUnsupported != null) {
67                     LogUtils.d(LogUtils.TAG, "Clearing unsupported policies "
68                             + account.mPolicy.mProtocolPoliciesUnsupported);
69                     account.mPolicy.mProtocolPoliciesUnsupported = null;
70                 }
71                 PolicyServiceProxy.setAccountPolicy2(context,
72                         account.getId(),
73                         account.mPolicy,
74                         account.mSecuritySyncKey == null ? "" : account.mSecuritySyncKey,
75                         false /* notify */);
76             }
77 
78             // Set up default quick responses here...
79             String[] defaultQuickResponses =
80                 context.getResources().getStringArray(R.array.default_quick_responses);
81             ContentValues cv = new ContentValues();
82             cv.put(QuickResponse.ACCOUNT_KEY, account.mId);
83             ContentResolver resolver = context.getContentResolver();
84             for (String quickResponse: defaultQuickResponses) {
85                 // Allow empty entries (some localizations may not want to have the maximum
86                 // number)
87                 if (!TextUtils.isEmpty(quickResponse)) {
88                     cv.put(QuickResponse.TEXT, quickResponse);
89                     resolver.insert(QuickResponse.CONTENT_URI, cv);
90                 }
91             }
92         } else {
93             ContentValues cv = getAccountContentValues(account);
94             account.update(context, cv);
95         }
96 
97         // Update the backup (side copy) of the accounts
98         AccountBackupRestore.backup(context);
99     }
100 
101     /**
102      * Returns a set of content values to commit account changes (not including the foreign keys
103      * for the two host auth's and policy) to the database.  Does not actually commit anything.
104      */
getAccountContentValues(Account account)105     public static ContentValues getAccountContentValues(Account account) {
106         ContentValues cv = new ContentValues();
107         cv.put(AccountColumns.DISPLAY_NAME, account.getDisplayName());
108         cv.put(AccountColumns.SENDER_NAME, account.getSenderName());
109         cv.put(AccountColumns.SIGNATURE, account.getSignature());
110         cv.put(AccountColumns.SYNC_INTERVAL, account.mSyncInterval);
111         cv.put(AccountColumns.FLAGS, account.mFlags);
112         cv.put(AccountColumns.SYNC_LOOKBACK, account.mSyncLookback);
113         cv.put(AccountColumns.SECURITY_SYNC_KEY, account.mSecuritySyncKey);
114         return cv;
115     }
116 
117    /**
118     * Create the request to get the authorization code.
119     *
120     * @param context
121     * @param provider The OAuth provider to register with
122     * @param emailAddress Email address to send as a hint to the oauth service.
123     * @return
124     */
createOAuthRegistrationRequest(final Context context, final OAuthProvider provider, final String emailAddress)125    public static Uri createOAuthRegistrationRequest(final Context context,
126            final OAuthProvider provider, final String emailAddress) {
127        final Uri.Builder b = Uri.parse(provider.authEndpoint).buildUpon();
128        b.appendQueryParameter("response_type", provider.responseType);
129        b.appendQueryParameter("client_id", provider.clientId);
130        b.appendQueryParameter("redirect_uri", provider.redirectUri);
131        b.appendQueryParameter("scope", provider.scope);
132        b.appendQueryParameter("state", provider.state);
133        b.appendQueryParameter("login_hint", emailAddress);
134        return b.build();
135    }
136 
137    /**
138     * Search for a single resource containing known oauth provider definitions.
139     *
140     * @param context
141     * @param id String Id of the oauth provider.
142     * @return The OAuthProvider if found, null if not.
143     */
findOAuthProvider(final Context context, final String id)144    public static OAuthProvider findOAuthProvider(final Context context, final String id) {
145        return findOAuthProvider(context, id, R.xml.oauth);
146    }
147 
getAllOAuthProviders(final Context context)148    public static List<OAuthProvider> getAllOAuthProviders(final Context context) {
149        try {
150            List<OAuthProvider> providers = new ArrayList<OAuthProvider>();
151            final XmlResourceParser xml = context.getResources().getXml(R.xml.oauth);
152            int xmlEventType;
153            OAuthProvider provider = null;
154            while ((xmlEventType = xml.next()) != XmlResourceParser.END_DOCUMENT) {
155                if (xmlEventType == XmlResourceParser.START_TAG
156                        && "provider".equals(xml.getName())) {
157                    try {
158                        provider = new OAuthProvider();
159                        provider.id = getXmlAttribute(context, xml, "id");
160                        provider.label = getXmlAttribute(context, xml, "label");
161                        provider.authEndpoint = getXmlAttribute(context, xml, "auth_endpoint");
162                        provider.tokenEndpoint = getXmlAttribute(context, xml, "token_endpoint");
163                        provider.refreshEndpoint = getXmlAttribute(context, xml,
164                                "refresh_endpoint");
165                        provider.responseType = getXmlAttribute(context, xml, "response_type");
166                        provider.redirectUri = getXmlAttribute(context, xml, "redirect_uri");
167                        provider.scope = getXmlAttribute(context, xml, "scope");
168                        provider.state = getXmlAttribute(context, xml, "state");
169                        provider.clientId = getXmlAttribute(context, xml, "client_id");
170                        provider.clientSecret = getXmlAttribute(context, xml, "client_secret");
171                        providers.add(provider);
172                    } catch (IllegalArgumentException e) {
173                        LogUtils.w(Logging.LOG_TAG, "providers line: " + xml.getLineNumber() +
174                                "; Domain contains multiple globals");
175                    }
176                }
177            }
178            return providers;
179        } catch (Exception e) {
180            LogUtils.e(Logging.LOG_TAG, "Error while trying to load provider settings.", e);
181        }
182        return null;
183    }
184 
185    /**
186     * Search for a single resource containing known oauth provider definitions.
187     *
188     * @param context
189     * @param id String Id of the oauth provider.
190     * @param resourceId ResourceId of the xml file to search.
191     * @return The OAuthProvider if found, null if not.
192     */
findOAuthProvider(final Context context, final String id, final int resourceId)193    public static OAuthProvider findOAuthProvider(final Context context, final String id,
194            final int resourceId) {
195        // TODO: Consider adding a way to cache this file during new account setup, so that we
196        // don't need to keep loading the file over and over.
197        // TODO: need a mechanism to get a list of all supported OAuth providers so that we can
198        // offer the user a choice of who to authenticate with.
199        try {
200            final XmlResourceParser xml = context.getResources().getXml(resourceId);
201            int xmlEventType;
202            OAuthProvider provider = null;
203            while ((xmlEventType = xml.next()) != XmlResourceParser.END_DOCUMENT) {
204                if (xmlEventType == XmlResourceParser.START_TAG
205                        && "provider".equals(xml.getName())) {
206                    String providerId = getXmlAttribute(context, xml, "id");
207                    try {
208                        if (TextUtils.equals(id, providerId)) {
209                            provider = new OAuthProvider();
210                            provider.id = id;
211                            provider.label = getXmlAttribute(context, xml, "label");
212                            provider.authEndpoint = getXmlAttribute(context, xml, "auth_endpoint");
213                            provider.tokenEndpoint = getXmlAttribute(context, xml, "token_endpoint");
214                            provider.refreshEndpoint = getXmlAttribute(context, xml,
215                                    "refresh_endpoint");
216                            provider.responseType = getXmlAttribute(context, xml, "response_type");
217                            provider.redirectUri = getXmlAttribute(context, xml, "redirect_uri");
218                            provider.scope = getXmlAttribute(context, xml, "scope");
219                            provider.state = getXmlAttribute(context, xml, "state");
220                            provider.clientId = getXmlAttribute(context, xml, "client_id");
221                            provider.clientSecret = getXmlAttribute(context, xml, "client_secret");
222                            return provider;
223                        }
224                    } catch (IllegalArgumentException e) {
225                        LogUtils.w(Logging.LOG_TAG, "providers line: " + xml.getLineNumber() +
226                                "; Domain contains multiple globals");
227                    }
228                }
229            }
230        } catch (Exception e) {
231            LogUtils.e(Logging.LOG_TAG, "Error while trying to load provider settings.", e);
232        }
233        return null;
234    }
235 
236    /**
237      * Search the list of known Email providers looking for one that matches the user's email
238      * domain.  We check for vendor supplied values first, then we look in providers_product.xml,
239      * and finally by the entries in platform providers.xml.  This provides a nominal override
240      * capability.
241      *
242      * A match is defined as any provider entry for which the "domain" attribute matches.
243      *
244      * @param domain The domain portion of the user's email address
245      * @return suitable Provider definition, or null if no match found
246      */
findProviderForDomain(Context context, String domain)247     public static Provider findProviderForDomain(Context context, String domain) {
248         Provider p = VendorPolicyLoader.getInstance(context).findProviderForDomain(domain);
249         if (p == null) {
250             p = findProviderForDomain(context, domain, R.xml.providers_product);
251         }
252         if (p == null) {
253             p = findProviderForDomain(context, domain, R.xml.providers);
254         }
255         return p;
256     }
257 
258     /**
259      * Search a single resource containing known Email provider definitions.
260      *
261      * @param domain The domain portion of the user's email address
262      * @param resourceId Id of the provider resource to scan
263      * @return suitable Provider definition, or null if no match found
264      */
findProviderForDomain( Context context, String domain, int resourceId)265     /*package*/ static Provider findProviderForDomain(
266             Context context, String domain, int resourceId) {
267         try {
268             XmlResourceParser xml = context.getResources().getXml(resourceId);
269             int xmlEventType;
270             Provider provider = null;
271             while ((xmlEventType = xml.next()) != XmlResourceParser.END_DOCUMENT) {
272                 if (xmlEventType == XmlResourceParser.START_TAG
273                         && "provider".equals(xml.getName())) {
274                     String providerDomain = getXmlAttribute(context, xml, "domain");
275                     try {
276                         if (matchProvider(domain, providerDomain)) {
277                             provider = new Provider();
278                             provider.id = getXmlAttribute(context, xml, "id");
279                             provider.label = getXmlAttribute(context, xml, "label");
280                             provider.domain = domain.toLowerCase();
281                             provider.note = getXmlAttribute(context, xml, "note");
282                             // TODO: Maybe this should actually do a lookup of the OAuth provider
283                             // here, and keep a pointer to it rather than a textual key.
284                             // To do this probably requires caching oauth.xml, otherwise the lookup
285                             // is expensive and likely to happen repeatedly.
286                             provider.oauth = getXmlAttribute(context, xml, "oauth");
287                         }
288                     } catch (IllegalArgumentException e) {
289                         LogUtils.w(Logging.LOG_TAG, "providers line: " + xml.getLineNumber() +
290                                 "; Domain contains multiple globals");
291                     }
292                 }
293                 else if (xmlEventType == XmlResourceParser.START_TAG
294                         && "incoming".equals(xml.getName())
295                         && provider != null) {
296                     provider.incomingUriTemplate = getXmlAttribute(context, xml, "uri");
297                     provider.incomingUsernameTemplate = getXmlAttribute(context, xml, "username");
298                 }
299                 else if (xmlEventType == XmlResourceParser.START_TAG
300                         && "outgoing".equals(xml.getName())
301                         && provider != null) {
302                     provider.outgoingUriTemplate = getXmlAttribute(context, xml, "uri");
303                     provider.outgoingUsernameTemplate = getXmlAttribute(context, xml, "username");
304                 }
305                 else if (xmlEventType == XmlResourceParser.START_TAG
306                         && "incoming-fallback".equals(xml.getName())
307                         && provider != null) {
308                     provider.altIncomingUriTemplate = getXmlAttribute(context, xml, "uri");
309                     provider.altIncomingUsernameTemplate =
310                             getXmlAttribute(context, xml, "username");
311                 }
312                 else if (xmlEventType == XmlResourceParser.START_TAG
313                         && "outgoing-fallback".equals(xml.getName())
314                         && provider != null) {
315                     provider.altOutgoingUriTemplate = getXmlAttribute(context, xml, "uri");
316                     provider.altOutgoingUsernameTemplate =
317                             getXmlAttribute(context, xml, "username");
318                 }
319                 else if (xmlEventType == XmlResourceParser.END_TAG
320                         && "provider".equals(xml.getName())
321                         && provider != null) {
322                     return provider;
323                 }
324             }
325         }
326         catch (Exception e) {
327             LogUtils.e(Logging.LOG_TAG, "Error while trying to load provider settings.", e);
328         }
329         return null;
330     }
331 
332     /**
333      * Returns true if the string <code>s1</code> matches the string <code>s2</code>. The string
334      * <code>s2</code> may contain any number of wildcards -- a '?' character -- and/or asterisk
335      * characters -- '*'. Wildcards match any single character, while the asterisk matches a domain
336      * part (i.e. substring demarcated by a period, '.')
337      */
338     @VisibleForTesting
matchProvider(String testDomain, String providerDomain)339     public static boolean matchProvider(String testDomain, String providerDomain) {
340         String[] testParts = testDomain.split(DOMAIN_SEPARATOR);
341         String[] providerParts = providerDomain.split(DOMAIN_SEPARATOR);
342         if (testParts.length != providerParts.length) {
343             return false;
344         }
345         for (int i = 0; i < testParts.length; i++) {
346             String testPart = testParts[i].toLowerCase();
347             String providerPart = providerParts[i].toLowerCase();
348             if (!providerPart.equals(WILD_STRING) &&
349                     !matchWithWildcards(testPart, providerPart)) {
350                 return false;
351             }
352         }
353         return true;
354     }
355 
matchWithWildcards(String testPart, String providerPart)356     private static boolean matchWithWildcards(String testPart, String providerPart) {
357         int providerLength = providerPart.length();
358         if (testPart.length() != providerLength){
359             return false;
360         }
361         for (int i = 0; i < providerLength; i++) {
362             char testChar = testPart.charAt(i);
363             char providerChar = providerPart.charAt(i);
364             if (testChar != providerChar && providerChar != WILD_CHARACTER) {
365                 return false;
366             }
367         }
368         return true;
369     }
370 
371     /**
372      * Attempts to get the given attribute as a String resource first, and if it fails
373      * returns the attribute as a simple String value.
374      * @param xml
375      * @param name
376      * @return the requested resource
377      */
getXmlAttribute(Context context, XmlResourceParser xml, String name)378     private static String getXmlAttribute(Context context, XmlResourceParser xml, String name) {
379         int resId = xml.getAttributeResourceValue(null, name, 0);
380         if (resId == 0) {
381             return xml.getAttributeValue(null, name);
382         }
383         else {
384             return context.getString(resId);
385         }
386     }
387 
388     /**
389      * Infer potential email server addresses from domain names
390      *
391      * Incoming: Prepend "imap" or "pop3" to domain, unless "pop", "pop3",
392      *          "imap", or "mail" are found.
393      * Outgoing: Prepend "smtp" if domain starts with any in the host prefix array
394      *
395      * @param server name as we know it so far
396      * @param incoming "pop3" or "imap" (or null)
397      * @param outgoing "smtp" or null
398      * @return the post-processed name for use in the UI
399      */
inferServerName(Context context, String server, String incoming, String outgoing)400     public static String inferServerName(Context context, String server, String incoming,
401             String outgoing) {
402         // Default values cause entire string to be kept, with prepended server string
403         int keepFirstChar = 0;
404         int firstDotIndex = server.indexOf('.');
405         if (firstDotIndex != -1) {
406             // look at first word and decide what to do
407             String firstWord = server.substring(0, firstDotIndex).toLowerCase();
408             String[] hostPrefixes =
409                     context.getResources().getStringArray(R.array.smtp_host_prefixes);
410             boolean canSubstituteSmtp = Utility.arrayContains(hostPrefixes, firstWord);
411             boolean isMail = "mail".equals(firstWord);
412             // Now decide what to do
413             if (incoming != null) {
414                 // For incoming, we leave imap/pop/pop3/mail alone, or prepend incoming
415                 if (canSubstituteSmtp || isMail) {
416                     return server;
417                 }
418             } else {
419                 // For outgoing, replace imap/pop/pop3 with outgoing, leave mail alone, or
420                 // prepend outgoing
421                 if (canSubstituteSmtp) {
422                     keepFirstChar = firstDotIndex + 1;
423                 } else if (isMail) {
424                     return server;
425                 } else {
426                     // prepend
427                 }
428             }
429         }
430         return ((incoming != null) ? incoming : outgoing) + '.' + server.substring(keepFirstChar);
431     }
432 
433 }
434