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
17package com.android.email.activity.setup;
18
19import android.content.ContentResolver;
20import android.content.ContentValues;
21import android.content.Context;
22import android.content.res.XmlResourceParser;
23import android.net.Uri;
24import android.text.TextUtils;
25
26import com.android.email.R;
27import com.android.email.provider.AccountBackupRestore;
28import com.android.emailcommon.Logging;
29import com.android.emailcommon.VendorPolicyLoader;
30import com.android.emailcommon.VendorPolicyLoader.OAuthProvider;
31import com.android.emailcommon.VendorPolicyLoader.Provider;
32import com.android.emailcommon.provider.Account;
33import com.android.emailcommon.provider.EmailContent.AccountColumns;
34import com.android.emailcommon.provider.QuickResponse;
35import com.android.emailcommon.service.PolicyServiceProxy;
36import com.android.emailcommon.utility.Utility;
37import com.android.mail.utils.LogUtils;
38import com.google.common.annotations.VisibleForTesting;
39
40import java.util.ArrayList;
41import java.util.List;
42
43public 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     */
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     */
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    */
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    */
144   public static OAuthProvider findOAuthProvider(final Context context, final String id) {
145       return findOAuthProvider(context, id, R.xml.oauth);
146   }
147
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    */
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     */
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     */
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
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
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     */
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     */
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