ExchangeStore.java revision faed6178b1858f0e6df388a409d14fb3f94afefa
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.Email;
20import com.android.email.mail.AuthenticationFailedException;
21import com.android.email.mail.Folder;
22import com.android.email.mail.MessagingException;
23import com.android.email.mail.Store;
24import com.android.email.mail.StoreSynchronizer;
25import com.android.email.provider.EmailContent.Account;
26import com.android.email.service.EasAuthenticatorService;
27import com.android.email.service.EmailServiceProxy;
28import com.android.exchange.Eas;
29import com.android.exchange.SyncManager;
30
31import android.accounts.AccountManager;
32import android.accounts.AccountManagerCallback;
33import android.accounts.AccountManagerFuture;
34import android.content.Context;
35import android.os.Bundle;
36import android.os.RemoteException;
37import android.text.TextUtils;
38
39import java.net.URI;
40import java.net.URISyntaxException;
41import 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 */
48public class ExchangeStore extends Store {
49    public static final String LOG_TAG = "ExchangeStore";
50
51    private 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 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                int result = new EmailServiceProxy(mContext, SyncManager.class)
244                    .validate("eas", mHost, mUsername, mPassword, port, ssl, tssl);
245                if (result != MessagingException.NO_ERROR) {
246                    if (result == MessagingException.AUTHENTICATION_FAILED) {
247                        throw new AuthenticationFailedException("Authentication failed.");
248                    } else {
249                        throw new MessagingException(result);
250                    }
251                }
252            } catch (RemoteException e) {
253                throw new MessagingException("Call to validate generated an exception", e);
254            }
255        }
256    }
257
258    /**
259     * We handle AutoDiscover for Exchange 2007 (and later) here, wrapping the EmailService call.
260     * The service call returns a HostAuth and we return null if there was a service issue
261     */
262    @Override
263    public Bundle autoDiscover(Context context, String username, String password)
264            throws MessagingException {
265        try {
266            return new EmailServiceProxy(context, SyncManager.class)
267                .autoDiscover(username, password);
268        } catch (RemoteException e) {
269            return null;
270        }
271    }
272}
273
274