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