ExchangeStore.java revision 17250429db16553b59d5df5e358f71406dd2b322
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.FetchProfile;
22import com.android.email.mail.Flag;
23import com.android.email.mail.Folder;
24import com.android.email.mail.Message;
25import com.android.email.mail.MessageRetrievalListener;
26import com.android.email.mail.MessagingException;
27import com.android.email.mail.Store;
28import com.android.email.mail.StoreSynchronizer;
29import com.android.email.service.EmailServiceProxy;
30import com.android.exchange.Eas;
31import com.android.exchange.IEmailService;
32import com.android.exchange.IEmailServiceCallback;
33import com.android.exchange.SyncManager;
34import com.android.exchange.EmailContent.Attachment;
35
36import android.accounts.AccountManager;
37import android.accounts.AuthenticatorException;
38import android.accounts.Future2;
39import android.accounts.Future2Callback;
40import android.accounts.OperationCanceledException;
41import android.content.Context;
42import android.os.Bundle;
43import android.os.IBinder;
44import android.os.RemoteException;
45import android.text.TextUtils;
46import android.util.Config;
47import android.util.Log;
48
49import java.io.IOException;
50import java.net.URI;
51import java.net.URISyntaxException;
52import java.util.HashMap;
53
54/**
55 * This is a placeholder for use in Exchange implementations.  It is based on the notion of
56 * lightweight adapter classes for Store, Folder, and Sender, and a common facade for the common
57 * Transport code.
58 */
59public class ExchangeStore extends Store {
60    public static final String LOG_TAG = "ExchangeStore";
61
62    private final Context mContext;
63    private URI mUri;
64    private PersistentDataCallbacks mCallbacks;
65
66    private final ExchangeTransport mTransport;
67    private final HashMap<String, Folder> mFolders = new HashMap<String, Folder>();
68
69    private boolean mPushModeRunning = false;
70
71    /**
72     * Factory method.
73     */
74    public static Store newInstance(String uri, Context context, PersistentDataCallbacks callbacks)
75    throws MessagingException {
76        return new ExchangeStore(uri, context, callbacks);
77    }
78
79    /**
80     * eas://user:password@server/domain
81     *
82     * @param _uri
83     * @param application
84     * @throws MessagingException
85     */
86    private ExchangeStore(String _uri, Context context, PersistentDataCallbacks callbacks)
87            throws MessagingException {
88        mContext = context;
89        try {
90            mUri = new URI(_uri);
91        } catch (URISyntaxException e) {
92            throw new MessagingException("Invalid uri for ExchangeStore");
93        }
94        mCallbacks = callbacks;
95
96        String scheme = mUri.getScheme();
97        int connectionSecurity;
98        if (scheme.equals("eas")) {
99            connectionSecurity = ExchangeTransport.CONNECTION_SECURITY_NONE;
100        } else if (scheme.equals("eas+ssl+")) {
101            connectionSecurity = ExchangeTransport.CONNECTION_SECURITY_SSL_REQUIRED;
102        } else {
103            throw new MessagingException("Unsupported protocol");
104        }
105
106        mTransport = ExchangeTransport.getInstance(mUri, context);
107    }
108
109    /**
110     * Retrieve the underlying transport.  Used primarily for testing.
111     * @return
112     */
113    /* package */ ExchangeTransport getTransport() {
114        return mTransport;
115    }
116
117    @Override
118    public void checkSettings() throws MessagingException {
119        mTransport.checkSettings(mUri);
120    }
121
122    @Override
123    public Folder getFolder(String name) throws MessagingException {
124        synchronized (mFolders) {
125            Folder folder = mFolders.get(name);
126            if (folder == null) {
127                folder = new ExchangeFolder(this, name);
128                mFolders.put(folder.getName(), folder);
129            }
130            return folder;
131        }
132    }
133
134    @Override
135    public Folder[] getPersonalNamespaces() throws MessagingException {
136        return new Folder[] {
137                getFolder(ExchangeTransport.FOLDER_INBOX),
138        };
139    }
140
141    /**
142     * For a store that supports push mode, this is the API that enables it or disables it.
143     * The store should use this API to start or stop its persistent connection service or thread.
144     *
145     * <p>Note, may be called multiple times, even after push mode has been started or stopped.
146     *
147     * @param enablePushMode start or stop push mode delivery
148     */
149    @Override
150    public void enablePushModeDelivery(boolean enablePushMode) {
151        if (Config.LOGD && Email.DEBUG) {
152            if (enablePushMode && !mPushModeRunning) {
153                Log.d(Email.LOG_TAG, "start push mode");
154            } else if (!enablePushMode && mPushModeRunning) {
155                Log.d(Email.LOG_TAG, "stop push mode");
156            } else {
157                Log.d(Email.LOG_TAG, enablePushMode ?
158                        "push mode already started" : "push mode already stopped");
159            }
160        }
161        mPushModeRunning = enablePushMode;
162    }
163
164    /**
165     * Get class of SettingActivity for this Store class.
166     * @return Activity class that has class method actionEditIncomingSettings()
167     */
168    @Override
169    public Class<? extends android.app.Activity> getSettingActivityClass() {
170        return com.android.email.activity.setup.AccountSetupExchange.class;
171    }
172
173    /**
174     * Get class of sync'er for this Store class.  Because exchange Sync rules are so different
175     * than IMAP or POP3, it's likely that an Exchange implementation will need its own sync
176     * controller.  If so, this function must return a non-null value.
177     *
178     * @return Message Sync controller, or null to use default
179     */
180    @Override
181    public StoreSynchronizer getMessageSynchronizer() {
182        return null;
183    }
184
185    /**
186     * Inform MessagingController that this store requires message structures to be prefetched
187     * before it can fetch message bodies (this is due to EAS protocol restrictions.)
188     * @return always true for EAS
189     */
190    @Override
191    public boolean requireStructurePrefetch() {
192        return true;
193    }
194
195    /**
196     * Inform MessagingController that messages sent via EAS will be placed in the Sent folder
197     * automatically (server-side) and don't need to be uploaded.
198     * @return always false for EAS (assuming server-side copy is supported)
199     */
200    @Override
201    public boolean requireCopyMessageToSentFolder() {
202        return false;
203    }
204
205    public static class ExchangeTransport {
206        public static final int CONNECTION_SECURITY_NONE = 0;
207        public static final int CONNECTION_SECURITY_SSL_REQUIRED = 1;
208
209        public static final String FOLDER_INBOX = Email.INBOX;
210
211        private static final String TAG = "ExchangeTransport";
212        private final Context mContext;
213
214        private String mHost;
215        private String mDomain;
216        private String mUsername;
217        private String mPassword;
218
219        private static HashMap<String, ExchangeTransport> sUriToInstanceMap =
220            new HashMap<String, ExchangeTransport>();
221        private static final HashMap<String, Integer> sFolderMap = new HashMap<String, Integer>();
222
223        /**
224         * Public factory.  The transport should be a singleton (per Uri)
225         */
226        public synchronized static ExchangeTransport getInstance(URI uri, Context context)
227        throws MessagingException {
228            if (!uri.getScheme().equals("eas") && !uri.getScheme().equals("eas+ssl+")) {
229                throw new MessagingException("Invalid scheme");
230            }
231
232            final String key = uri.toString();
233            ExchangeTransport transport = sUriToInstanceMap.get(key);
234            if (transport == null) {
235                transport = new ExchangeTransport(uri, context);
236                sUriToInstanceMap.put(key, transport);
237            }
238            return transport;
239        }
240
241        /**
242         * Private constructor - use public factory.
243         */
244        private ExchangeTransport(URI uri, Context context) throws MessagingException {
245            mContext = context;
246            setUri(uri);
247        }
248
249        /**
250         * Use the Uri to set up a newly-constructed transport
251         * @param uri
252         * @throws MessagingException
253         */
254        private void setUri(final URI uri) throws MessagingException {
255            mHost = uri.getHost();
256            if (mHost == null) {
257                throw new MessagingException("host not specified");
258            }
259
260            mDomain = uri.getPath();
261            if (!TextUtils.isEmpty(mDomain)) {
262                mDomain = mDomain.substring(1);
263            }
264
265            final String userInfo = uri.getUserInfo();
266            if (userInfo == null) {
267                throw new MessagingException("user information not specifed");
268            }
269            final String[] uinfo = userInfo.split(":", 2);
270            if (uinfo.length != 2) {
271                throw new MessagingException("user name and password not specified");
272            }
273            mUsername = uinfo[0];
274            mPassword = uinfo[1];
275        }
276
277        /**
278         * Blocking call that checks for a useable server connection, credentials, etc.
279         * @param uri the server/account to try and connect to
280         * @throws MessagingException thrown if the connection, server, account are not useable
281         */
282
283        IEmailServiceCallback mCallback = new IEmailServiceCallback () {
284
285            public void status(int statusCode, int progress) throws RemoteException {
286                Log.d("Status: ", "Code = " + statusCode + ", progress = " + progress);
287            }
288
289            public IBinder asBinder() { return null; }
290        };
291
292        /**
293         * Here's where we check the settings for EAS.
294         * @param uri the URI of the account to create
295         * @throws MessagingException if we can't authenticate the account
296         */
297        public void checkSettings(URI uri) throws MessagingException {
298            setUri(uri);
299            boolean ssl = uri.getScheme().contains("ssl+");
300            try {
301                IEmailService svc = EmailServiceProxy.getService(mContext, SyncManager.class);
302                int result = svc.validate("eas", mHost, mUsername, mPassword, ssl ? 443 : 80, ssl);
303                if (result != MessagingException.NO_ERROR) {
304                    if (result == MessagingException.AUTHENTICATION_FAILED) {
305                        throw new AuthenticationFailedException("Authentication failed.");
306                    } else {
307                        throw new MessagingException(result);
308                    }
309                } else {
310                    // This code was taken from sample code in AccountsTester
311                    Bundle options = new Bundle();
312                    options.putString("username", mUsername);
313                    options.putString("password", mPassword);
314                    Future2Callback callback = new Future2Callback() {
315                        public void run(Future2 future) {
316                            try {
317                                Bundle bundle = future.getResult();
318                                bundle.keySet();
319                                Log.d(TAG, "account added: " + bundle);
320                            } catch (OperationCanceledException e) {
321                                Log.d(TAG, "addAccount was canceled");
322                            } catch (IOException e) {
323                                Log.d(TAG, "addAccount failed: " + e);
324                            } catch (AuthenticatorException e) {
325                                Log.d(TAG, "addAccount failed: " + e);
326                            }
327
328                        }
329                    };
330                    // Here's where we tell AccountManager about the new account.  The addAccount
331                    // method in AccountManager calls the addAccount method in our authenticator
332                    // service (EasAuthenticatorService)
333                    AccountManager.get(mContext).addAccount(Eas.ACCOUNT_MANAGER_TYPE, null, null,
334                            options, null, callback, null);
335                 }
336                svc.loadAttachment(0, new Attachment(), mCallback);
337             } catch (RemoteException e) {
338                throw new MessagingException("Call to validate generated an exception", e);
339            }
340        }
341
342        /**
343         * Typical helper function:  Return existence of a given folder
344         */
345        public boolean isFolderAvailable(final String folder) {
346            return sFolderMap.containsKey(folder);
347        }
348    }
349
350    public static class ExchangeFolder extends Folder {
351
352        private final ExchangeTransport mTransport;
353        @SuppressWarnings("unused")
354        private final ExchangeStore mStore;
355        @SuppressWarnings("unused")
356        private final String mName;
357
358        @SuppressWarnings("unused")
359        private PersistentDataCallbacks mPersistenceCallbacks;
360
361        public ExchangeFolder(ExchangeStore store, String name)
362                throws MessagingException {
363            mStore = store;
364            mTransport = store.getTransport();
365            mName = name;
366            if (!mTransport.isFolderAvailable(name)) {
367                throw new MessagingException("folder not supported: " + name);
368            }
369        }
370
371        @Override
372        public void appendMessages(Message[] messages) throws MessagingException {
373            // TODO Implement this function
374        }
375
376        @Override
377        public void close(boolean expunge) throws MessagingException {
378            mPersistenceCallbacks = null;
379            // TODO Implement this function
380        }
381
382        @Override
383        public void copyMessages(Message[] msgs, Folder folder, MessageUpdateCallbacks callbacks)
384                throws MessagingException {
385            // TODO Implement this function
386        }
387
388        @Override
389        public boolean create(FolderType type) throws MessagingException {
390            // TODO Implement this function
391            return false;
392        }
393
394        @Override
395        public void delete(boolean recurse) throws MessagingException {
396            // TODO Implement this function
397        }
398
399        @Override
400        public boolean exists() throws MessagingException {
401            // TODO Implement this function
402            return false;
403        }
404
405        @Override
406        public Message[] expunge() throws MessagingException {
407            // TODO Implement this function
408            return null;
409        }
410
411        @Override
412        public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)
413                throws MessagingException {
414            // TODO Implement this function
415        }
416
417        @Override
418        public Message getMessage(String uid) throws MessagingException {
419            // TODO Implement this function
420            return null;
421        }
422
423        @Override
424        public int getMessageCount() throws MessagingException {
425            // TODO Implement this function
426            return 0;
427        }
428
429        @Override
430        public Message[] getMessages(int start, int end, MessageRetrievalListener listener)
431                throws MessagingException {
432            // TODO Implement this function
433            return null;
434        }
435
436        @Override
437        public Message[] getMessages(MessageRetrievalListener listener) throws MessagingException {
438            // TODO Implement this function
439            return null;
440        }
441
442        @Override
443        public Message[] getMessages(String[] uids, MessageRetrievalListener listener)
444                throws MessagingException {
445            // TODO Implement this function
446            return null;
447        }
448
449        @Override
450        public OpenMode getMode() throws MessagingException {
451            // TODO Implement this function
452            return null;
453        }
454
455        @Override
456        public String getName() {
457            // TODO Implement this function
458            return null;
459        }
460
461        @Override
462        public Flag[] getPermanentFlags() throws MessagingException {
463            // TODO Implement this function
464            return null;
465        }
466
467        @Override
468        public int getUnreadMessageCount() throws MessagingException {
469            // TODO Implement this function
470            return 0;
471        }
472
473        @Override
474        public boolean isOpen() {
475            // TODO Implement this function
476            return false;
477        }
478
479        @Override
480        public void open(OpenMode mode, PersistentDataCallbacks callbacks)
481                throws MessagingException {
482            mPersistenceCallbacks = callbacks;
483            // TODO Implement this function
484        }
485
486        @Override
487        public void setFlags(Message[] messages, Flag[] flags, boolean value)
488                throws MessagingException {
489            // TODO Implement this function
490        }
491
492
493    }
494
495
496}
497
498