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