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.mail.store; 18 19 import com.android.email.Email; 20 import com.android.email.ExchangeUtils; 21 import com.android.email.mail.AuthenticationFailedException; 22 import com.android.email.mail.Folder; 23 import com.android.email.mail.MessagingException; 24 import com.android.email.mail.Store; 25 import com.android.email.mail.StoreSynchronizer; 26 import com.android.email.provider.EmailContent.Account; 27 import com.android.email.service.EasAuthenticatorService; 28 import com.android.email.service.EmailServiceProxy; 29 import com.android.email.service.IEmailService; 30 31 import android.accounts.AccountManager; 32 import android.accounts.AccountManagerCallback; 33 import android.accounts.AccountManagerFuture; 34 import android.content.Context; 35 import android.os.Bundle; 36 import android.os.RemoteException; 37 import android.text.TextUtils; 38 39 import java.net.URI; 40 import java.net.URISyntaxException; 41 import java.util.HashMap; 42 43 /** 44 * Our Exchange service does not use the sender/store model. This class exists for exactly two 45 * purposes, (1) to provide a hook for checking account connections, and (2) to return 46 * "AccountSetupExchange.class" for getSettingActivityClass(). 47 */ 48 public class ExchangeStore extends Store { 49 public static final String LOG_TAG = "ExchangeStore"; 50 51 private final URI mUri; 52 private final ExchangeTransport mTransport; 53 54 /** 55 * Factory method. 56 */ 57 public static Store newInstance(String uri, Context context, PersistentDataCallbacks callbacks) 58 throws MessagingException { 59 return new ExchangeStore(uri, context, callbacks); 60 } 61 62 /** 63 * eas://user:password@server/domain 64 * 65 * @param _uri 66 * @param application 67 */ 68 private ExchangeStore(String _uri, Context context, PersistentDataCallbacks callbacks) 69 throws MessagingException { 70 try { 71 mUri = new URI(_uri); 72 } catch (URISyntaxException e) { 73 throw new MessagingException("Invalid uri for ExchangeStore"); 74 } 75 76 mTransport = ExchangeTransport.getInstance(mUri, context); 77 } 78 79 @Override 80 public void checkSettings() throws MessagingException { 81 mTransport.checkSettings(mUri); 82 } 83 84 static public AccountManagerFuture<Bundle> addSystemAccount(Context context, Account acct, 85 boolean syncContacts, boolean syncCalendar, AccountManagerCallback<Bundle> callback) { 86 // Create a description of the new account 87 Bundle options = new Bundle(); 88 options.putString(EasAuthenticatorService.OPTIONS_USERNAME, acct.mEmailAddress); 89 options.putString(EasAuthenticatorService.OPTIONS_PASSWORD, acct.mHostAuthRecv.mPassword); 90 options.putBoolean(EasAuthenticatorService.OPTIONS_CONTACTS_SYNC_ENABLED, syncContacts); 91 options.putBoolean(EasAuthenticatorService.OPTIONS_CALENDAR_SYNC_ENABLED, syncCalendar); 92 93 // Here's where we tell AccountManager about the new account. The addAccount 94 // method in AccountManager calls the addAccount method in our authenticator 95 // service (EasAuthenticatorService) 96 return AccountManager.get(context).addAccount(Email.EXCHANGE_ACCOUNT_MANAGER_TYPE, 97 null, null, options, null, callback, null); 98 } 99 100 /** 101 * Remove an account from the Account manager - see {@link AccountManager#removeAccount( 102 * android.accounts.Account, AccountManagerCallback, android.os.Handler)}. 103 * 104 * @param context context to use 105 * @param acct the account to remove 106 * @param callback async results callback - pass null to use blocking mode 107 */ 108 static public AccountManagerFuture<Boolean> removeSystemAccount(Context context, Account acct, 109 AccountManagerCallback<Bundle> callback) { 110 android.accounts.Account systemAccount = 111 new android.accounts.Account(acct.mEmailAddress, Email.EXCHANGE_ACCOUNT_MANAGER_TYPE); 112 return AccountManager.get(context).removeAccount(systemAccount, null, null); 113 } 114 115 @Override 116 public Folder getFolder(String name) { 117 return null; 118 } 119 120 @Override 121 public Folder[] getPersonalNamespaces() { 122 return null; 123 } 124 125 /** 126 * Get class of SettingActivity for this Store class. 127 * @return Activity class that has class method actionEditIncomingSettings() 128 */ 129 @Override 130 public Class<? extends android.app.Activity> getSettingActivityClass() { 131 return com.android.email.activity.setup.AccountSetupExchange.class; 132 } 133 134 /** 135 * Get class of sync'er for this Store class. Because exchange Sync rules are so different 136 * than IMAP or POP3, it's likely that an Exchange implementation will need its own sync 137 * controller. If so, this function must return a non-null value. 138 * 139 * @return Message Sync controller, or null to use default 140 */ 141 @Override 142 public StoreSynchronizer getMessageSynchronizer() { 143 return null; 144 } 145 146 /** 147 * Inform MessagingController that this store requires message structures to be prefetched 148 * before it can fetch message bodies (this is due to EAS protocol restrictions.) 149 * @return always true for EAS 150 */ 151 @Override 152 public boolean requireStructurePrefetch() { 153 return true; 154 } 155 156 /** 157 * Inform MessagingController that messages sent via EAS will be placed in the Sent folder 158 * automatically (server-side) and don't need to be uploaded. 159 * @return always false for EAS (assuming server-side copy is supported) 160 */ 161 @Override 162 public boolean requireCopyMessageToSentFolder() { 163 return false; 164 } 165 166 public static class ExchangeTransport { 167 private final Context mContext; 168 169 private String mHost; 170 private String mDomain; 171 private String mUsername; 172 private String mPassword; 173 174 private static final HashMap<String, ExchangeTransport> sUriToInstanceMap = 175 new HashMap<String, ExchangeTransport>(); 176 177 /** 178 * Public factory. The transport should be a singleton (per Uri) 179 */ 180 public synchronized static ExchangeTransport getInstance(URI uri, Context context) 181 throws MessagingException { 182 if (!uri.getScheme().equals("eas") && !uri.getScheme().equals("eas+ssl+") && 183 !uri.getScheme().equals("eas+ssl+trustallcerts")) { 184 throw new MessagingException("Invalid scheme"); 185 } 186 187 final String key = uri.toString(); 188 ExchangeTransport transport = sUriToInstanceMap.get(key); 189 if (transport == null) { 190 transport = new ExchangeTransport(uri, context); 191 sUriToInstanceMap.put(key, transport); 192 } 193 return transport; 194 } 195 196 /** 197 * Private constructor - use public factory. 198 */ 199 private ExchangeTransport(URI uri, Context context) throws MessagingException { 200 mContext = context; 201 setUri(uri); 202 } 203 204 /** 205 * Use the Uri to set up a newly-constructed transport 206 * @param uri 207 * @throws MessagingException 208 */ 209 private void setUri(final URI uri) throws MessagingException { 210 mHost = uri.getHost(); 211 if (mHost == null) { 212 throw new MessagingException("host not specified"); 213 } 214 215 mDomain = uri.getPath(); 216 if (!TextUtils.isEmpty(mDomain)) { 217 mDomain = mDomain.substring(1); 218 } 219 220 final String userInfo = uri.getUserInfo(); 221 if (userInfo == null) { 222 throw new MessagingException("user information not specifed"); 223 } 224 final String[] uinfo = userInfo.split(":", 2); 225 if (uinfo.length != 2) { 226 throw new MessagingException("user name and password not specified"); 227 } 228 mUsername = uinfo[0]; 229 mPassword = uinfo[1]; 230 } 231 232 /** 233 * Here's where we check the settings for EAS. 234 * @param uri the URI of the account to create 235 * @throws MessagingException if we can't authenticate the account 236 */ 237 public void checkSettings(URI uri) throws MessagingException { 238 setUri(uri); 239 boolean ssl = uri.getScheme().contains("+ssl"); 240 boolean tssl = uri.getScheme().contains("+trustallcerts"); 241 try { 242 int port = ssl ? 443 : 80; 243 244 IEmailService svc = ExchangeUtils.getExchangeEmailService(mContext, null); 245 // Use a longer timeout for the validate command. Note that the instanceof check 246 // shouldn't be necessary; we'll do it anyway, just to be safe 247 if (svc instanceof EmailServiceProxy) { 248 ((EmailServiceProxy)svc).setTimeout(90); 249 } 250 int result = svc.validate("eas", mHost, mUsername, mPassword, port, ssl, tssl); 251 if (result != MessagingException.NO_ERROR) { 252 if (result == MessagingException.AUTHENTICATION_FAILED) { 253 throw new AuthenticationFailedException("Authentication failed."); 254 } else { 255 throw new MessagingException(result); 256 } 257 } 258 } catch (RemoteException e) { 259 throw new MessagingException("Call to validate generated an exception", e); 260 } 261 } 262 } 263 264 /** 265 * We handle AutoDiscover for Exchange 2007 (and later) here, wrapping the EmailService call. 266 * The service call returns a HostAuth and we return null if there was a service issue 267 */ 268 @Override 269 public Bundle autoDiscover(Context context, String username, String password) 270 throws MessagingException { 271 try { 272 return ExchangeUtils.getExchangeEmailService(context, null) 273 .autoDiscover(username, password); 274 } catch (RemoteException e) { 275 return null; 276 } 277 } 278 } 279