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 */ 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