EmailServiceUtils.java revision e4ac06080ba8b8e57531982ea290354b1a9a4305
1/*
2 * Copyright (C) 2010 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.service;
18
19import android.accounts.AccountManager;
20import android.accounts.AccountManagerCallback;
21import android.accounts.AccountManagerFuture;
22import android.accounts.AuthenticatorException;
23import android.accounts.OperationCanceledException;
24import android.app.Service;
25import android.content.ContentProviderClient;
26import android.content.ContentResolver;
27import android.content.ContentUris;
28import android.content.ContentValues;
29import android.content.Context;
30import android.content.Intent;
31import android.content.res.Resources;
32import android.content.res.TypedArray;
33import android.content.res.XmlResourceParser;
34import android.database.Cursor;
35import android.net.Uri;
36import android.os.Bundle;
37import android.os.IBinder;
38import android.os.RemoteException;
39import android.provider.CalendarContract;
40import android.provider.CalendarContract.Calendars;
41import android.provider.CalendarContract.SyncState;
42import android.provider.ContactsContract;
43import android.provider.SyncStateContract;
44
45import com.android.email.R;
46import com.android.emailcommon.Api;
47import com.android.emailcommon.Logging;
48import com.android.emailcommon.provider.Account;
49import com.android.emailcommon.provider.EmailContent;
50import com.android.emailcommon.provider.EmailContent.AccountColumns;
51import com.android.emailcommon.provider.HostAuth;
52import com.android.emailcommon.service.EmailServiceProxy;
53import com.android.emailcommon.service.IEmailService;
54import com.android.emailcommon.service.IEmailServiceCallback;
55import com.android.emailcommon.service.SearchParams;
56import com.android.emailcommon.service.SyncWindow;
57import com.android.mail.utils.LogUtils;
58import com.google.common.collect.ImmutableMap;
59
60import org.xmlpull.v1.XmlPullParserException;
61
62import java.io.IOException;
63import java.util.Collection;
64import java.util.Map;
65
66/**
67 * Utility functions for EmailService support.
68 */
69public class EmailServiceUtils {
70    private static Map<String, EmailServiceInfo> sServiceMap = null;
71
72    /**
73     * Starts an EmailService by protocol
74     */
75    public static void startService(Context context, String protocol) {
76        EmailServiceInfo info = getServiceInfo(context, protocol);
77        if (info != null && info.intentAction != null) {
78            final Intent serviceIntent = getServiceIntent(info);
79            context.startService(serviceIntent);
80        }
81    }
82
83    /**
84     * Starts all remote services
85     */
86    public static void startRemoteServices(Context context) {
87        for (EmailServiceInfo info: getServiceInfoList(context)) {
88            if (info.intentAction != null) {
89                final Intent serviceIntent = getServiceIntent(info);
90                context.startService(serviceIntent);
91            }
92        }
93    }
94
95    /**
96     * Returns whether or not remote services are present on device
97     */
98    public static boolean areRemoteServicesInstalled(Context context) {
99        for (EmailServiceInfo info: getServiceInfoList(context)) {
100            if (info.intentAction != null) {
101                return true;
102            }
103        }
104        return false;
105    }
106
107    /**
108     * Starts all remote services
109     */
110    public static void setRemoteServicesLogging(Context context, int debugBits) {
111        for (EmailServiceInfo info: getServiceInfoList(context)) {
112            if (info.intentAction != null) {
113                EmailServiceProxy service =
114                        EmailServiceUtils.getService(context, info.protocol);
115                if (service != null) {
116                    try {
117                        service.setLogging(debugBits);
118                    } catch (RemoteException e) {
119                        // Move along, nothing to see
120                    }
121                }
122            }
123        }
124    }
125
126    /**
127     * Determine if the EmailService is available
128     */
129    public static boolean isServiceAvailable(Context context, String protocol) {
130        EmailServiceInfo info = getServiceInfo(context, protocol);
131        if (info == null) return false;
132        if (info.klass != null) return true;
133        final Intent serviceIntent = getServiceIntent(info);
134        return new EmailServiceProxy(context, serviceIntent).test();
135    }
136
137    private static Intent getServiceIntent(EmailServiceInfo info) {
138        final Intent serviceIntent = new Intent(info.intentAction);
139        serviceIntent.setPackage(info.intentPackage);
140        return serviceIntent;
141    }
142
143    /**
144     * For a given account id, return a service proxy if applicable, or null.
145     *
146     * @param accountId the message of interest
147     * @result service proxy, or null if n/a
148     */
149    public static EmailServiceProxy getServiceForAccount(Context context, long accountId) {
150        return getService(context, Account.getProtocol(context, accountId));
151    }
152
153    /**
154     * Holder of service information (currently just name and class/intent); if there is a class
155     * member, this is a (local, i.e. same process) service; otherwise, this is a remote service
156     */
157    public static class EmailServiceInfo {
158        public String protocol;
159        public String name;
160        public String accountType;
161        Class<? extends Service> klass;
162        String intentAction;
163        String intentPackage;
164        public int port;
165        public int portSsl;
166        public boolean defaultSsl;
167        public boolean offerTls;
168        public boolean offerCerts;
169        public boolean usesSmtp;
170        public boolean offerLocalDeletes;
171        public int defaultLocalDeletes;
172        public boolean offerPrefix;
173        public boolean usesAutodiscover;
174        public boolean offerLookback;
175        public int defaultLookback;
176        public boolean syncChanges;
177        public boolean syncContacts;
178        public boolean syncCalendar;
179        public boolean offerAttachmentPreload;
180        public CharSequence[] syncIntervalStrings;
181        public CharSequence[] syncIntervals;
182        public int defaultSyncInterval;
183        public String inferPrefix;
184        public boolean offerLoadMore;
185        public boolean requiresSetup;
186        public boolean hide;
187
188        @Override
189        public String toString() {
190            StringBuilder sb = new StringBuilder("Protocol: ");
191            sb.append(protocol);
192            sb.append(", ");
193            sb.append(klass != null ? "Local" : "Remote");
194            sb.append(" , Account Type: ");
195            sb.append(accountType);
196            return sb.toString();
197        }
198    }
199
200    public static EmailServiceProxy getService(Context context, String protocol) {
201        EmailServiceInfo info = null;
202        // Handle the degenerate case here (account might have been deleted)
203        if (protocol != null) {
204            info = getServiceInfo(context, protocol);
205        }
206        if (info == null) {
207            LogUtils.w(Logging.LOG_TAG, "Returning NullService for " + protocol);
208            return new EmailServiceProxy(context, NullService.class);
209        } else  {
210            return getServiceFromInfo(context, info);
211        }
212    }
213
214    public static EmailServiceProxy getServiceFromInfo(Context context, EmailServiceInfo info) {
215        if (info.klass != null) {
216            return new EmailServiceProxy(context, info.klass);
217        } else {
218            final Intent serviceIntent = getServiceIntent(info);
219            return new EmailServiceProxy(context, serviceIntent);
220        }
221    }
222
223    public static EmailServiceInfo getServiceInfoForAccount(Context context, long accountId) {
224        String protocol = Account.getProtocol(context, accountId);
225        return getServiceInfo(context, protocol);
226    }
227
228    public static synchronized EmailServiceInfo getServiceInfo(Context context, String protocol) {
229        if (sServiceMap == null) {
230            findServices(context);
231        }
232        return sServiceMap.get(protocol);
233    }
234
235    public static synchronized Collection<EmailServiceInfo> getServiceInfoList(Context context) {
236        if (sServiceMap == null) {
237            findServices(context);
238        }
239        return sServiceMap.values();
240    }
241
242    private static void finishAccountManagerBlocker(AccountManagerFuture<?> future) {
243        try {
244            // Note: All of the potential errors are simply logged
245            // here, as there is nothing to actually do about them.
246            future.getResult();
247        } catch (OperationCanceledException e) {
248            LogUtils.w(Logging.LOG_TAG, e.toString());
249        } catch (AuthenticatorException e) {
250            LogUtils.w(Logging.LOG_TAG, e.toString());
251        } catch (IOException e) {
252            LogUtils.w(Logging.LOG_TAG, e.toString());
253        }
254    }
255
256    /**
257     * Add an account to the AccountManager.
258     * @param context Our {@link Context}.
259     * @param account The {@link Account} we're adding.
260     * @param email Whether the user wants to sync email on this account.
261     * @param calendar Whether the user wants to sync calendar on this account.
262     * @param contacts Whether the user wants to sync contacts on this account.
263     * @param callback A callback for when the AccountManager is done.
264     * @return The result of {@link AccountManager#addAccount}.
265     */
266    public static AccountManagerFuture<Bundle> setupAccountManagerAccount(final Context context,
267            final Account account, final boolean email, final boolean calendar,
268            final boolean contacts, final AccountManagerCallback<Bundle> callback) {
269        final Bundle options = new Bundle(5);
270        final HostAuth hostAuthRecv =
271                HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv);
272        if (hostAuthRecv == null) {
273            return null;
274        }
275        // Set up username/password
276        options.putString(EasAuthenticatorService.OPTIONS_USERNAME, account.mEmailAddress);
277        options.putString(EasAuthenticatorService.OPTIONS_PASSWORD, hostAuthRecv.mPassword);
278        options.putBoolean(EasAuthenticatorService.OPTIONS_CONTACTS_SYNC_ENABLED, contacts);
279        options.putBoolean(EasAuthenticatorService.OPTIONS_CALENDAR_SYNC_ENABLED, calendar);
280        options.putBoolean(EasAuthenticatorService.OPTIONS_EMAIL_SYNC_ENABLED, email);
281        final EmailServiceInfo info = getServiceInfo(context, hostAuthRecv.mProtocol);
282        return AccountManager.get(context).addAccount(info.accountType, null, null, options, null,
283                callback, null);
284    }
285
286    public static void updateAccountManagerType(Context context,
287            android.accounts.Account amAccount, final Map<String, String> protocolMap) {
288        final ContentResolver resolver = context.getContentResolver();
289        final Cursor c = resolver.query(Account.CONTENT_URI, Account.CONTENT_PROJECTION,
290                AccountColumns.EMAIL_ADDRESS + "=?", new String[] { amAccount.name }, null);
291        // That's odd, isn't it?
292        if (c == null) return;
293        try {
294            if (c.moveToNext()) {
295                // Get the EmailProvider Account/HostAuth
296                final Account account = new Account();
297                account.restore(c);
298                final HostAuth hostAuth =
299                        HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv);
300                if (hostAuth == null) {
301                    return;
302                }
303
304                final String newProtocol = protocolMap.get(hostAuth.mProtocol);
305                if (newProtocol == null) {
306                    // This account doesn't need updating.
307                    return;
308                }
309
310                LogUtils.w(Logging.LOG_TAG, "Converting " + amAccount.name + " to "
311                        + newProtocol);
312
313                final ContentValues accountValues = new ContentValues();
314                int oldFlags = account.mFlags;
315
316                // Mark the provider account incomplete so it can't get reconciled away
317                account.mFlags |= Account.FLAGS_INCOMPLETE;
318                accountValues.put(AccountColumns.FLAGS, account.mFlags);
319                final Uri accountUri = ContentUris.withAppendedId(Account.CONTENT_URI, account.mId);
320                resolver.update(accountUri, accountValues, null, null);
321
322                // Change the HostAuth to reference the new protocol; this has to be done before
323                // trying to create the AccountManager account (below)
324                final ContentValues hostValues = new ContentValues();
325                hostValues.put(HostAuth.PROTOCOL, newProtocol);
326                resolver.update(ContentUris.withAppendedId(HostAuth.CONTENT_URI, hostAuth.mId),
327                        hostValues, null, null);
328                LogUtils.w(Logging.LOG_TAG, "Updated HostAuths");
329
330                try {
331                    // Get current settings for the existing AccountManager account
332                    boolean email = ContentResolver.getSyncAutomatically(amAccount,
333                            EmailContent.AUTHORITY);
334                    if (!email) {
335                        // Try our old provider name
336                        email = ContentResolver.getSyncAutomatically(amAccount,
337                                "com.android.email.provider");
338                    }
339                    final boolean contacts = ContentResolver.getSyncAutomatically(amAccount,
340                            ContactsContract.AUTHORITY);
341                    final boolean calendar = ContentResolver.getSyncAutomatically(amAccount,
342                            CalendarContract.AUTHORITY);
343                    LogUtils.w(Logging.LOG_TAG, "Email: " + email + ", Contacts: " + contacts + ","
344                            + " Calendar: " + calendar);
345
346                    // Get sync keys for calendar/contacts
347                    final String amName = amAccount.name;
348                    final String oldType = amAccount.type;
349                    ContentProviderClient client = context.getContentResolver()
350                            .acquireContentProviderClient(CalendarContract.CONTENT_URI);
351                    byte[] calendarSyncKey = null;
352                    try {
353                        calendarSyncKey = SyncStateContract.Helpers.get(client,
354                                asCalendarSyncAdapter(SyncState.CONTENT_URI, amName, oldType),
355                                new android.accounts.Account(amName, oldType));
356                    } catch (RemoteException e) {
357                        LogUtils.w(Logging.LOG_TAG, "Get calendar key FAILED");
358                    } finally {
359                        client.release();
360                    }
361                    client = context.getContentResolver()
362                            .acquireContentProviderClient(ContactsContract.AUTHORITY_URI);
363                    byte[] contactsSyncKey = null;
364                    try {
365                        contactsSyncKey = SyncStateContract.Helpers.get(client,
366                                ContactsContract.SyncState.CONTENT_URI,
367                                new android.accounts.Account(amName, oldType));
368                    } catch (RemoteException e) {
369                        LogUtils.w(Logging.LOG_TAG, "Get contacts key FAILED");
370                    } finally {
371                        client.release();
372                    }
373                    if (calendarSyncKey != null) {
374                        LogUtils.w(Logging.LOG_TAG, "Got calendar key: "
375                                + new String(calendarSyncKey));
376                    }
377                    if (contactsSyncKey != null) {
378                        LogUtils.w(Logging.LOG_TAG, "Got contacts key: "
379                                + new String(contactsSyncKey));
380                    }
381
382                    // Set up a new AccountManager account with new type and old settings
383                    AccountManagerFuture<?> amFuture = setupAccountManagerAccount(context, account,
384                            email, calendar, contacts, null);
385                    finishAccountManagerBlocker(amFuture);
386                    LogUtils.w(Logging.LOG_TAG, "Created new AccountManager account");
387
388                    // Delete the AccountManager account
389                    amFuture = AccountManager.get(context)
390                            .removeAccount(amAccount, null, null);
391                    finishAccountManagerBlocker(amFuture);
392                    LogUtils.w(Logging.LOG_TAG, "Deleted old AccountManager account");
393
394                    // Restore sync keys for contacts/calendar
395                    // TODO: Clean up how we determine the type.
396                    final String accountType = protocolMap.get(hostAuth.mProtocol + "_type");
397                    if (accountType != null &&
398                            calendarSyncKey != null && calendarSyncKey.length != 0) {
399                        client = context.getContentResolver()
400                                .acquireContentProviderClient(CalendarContract.CONTENT_URI);
401                        try {
402                            SyncStateContract.Helpers.set(client,
403                                    asCalendarSyncAdapter(SyncState.CONTENT_URI, amName,
404                                            accountType),
405                                    new android.accounts.Account(amName, accountType),
406                                    calendarSyncKey);
407                            LogUtils.w(Logging.LOG_TAG, "Set calendar key...");
408                        } catch (RemoteException e) {
409                            LogUtils.w(Logging.LOG_TAG, "Set calendar key FAILED");
410                        } finally {
411                            client.release();
412                        }
413                    }
414                    if (accountType != null &&
415                            contactsSyncKey != null && contactsSyncKey.length != 0) {
416                        client = context.getContentResolver()
417                                .acquireContentProviderClient(ContactsContract.AUTHORITY_URI);
418                        try {
419                            SyncStateContract.Helpers.set(client,
420                                    ContactsContract.SyncState.CONTENT_URI,
421                                    new android.accounts.Account(amName, accountType),
422                                    contactsSyncKey);
423                            LogUtils.w(Logging.LOG_TAG, "Set contacts key...");
424                        } catch (RemoteException e) {
425                            LogUtils.w(Logging.LOG_TAG, "Set contacts key FAILED");
426                        }
427                    }
428
429                    // That's all folks!
430                    LogUtils.w(Logging.LOG_TAG, "Account update completed.");
431                } finally {
432                    // Clear the incomplete flag on the provider account
433                    accountValues.put(AccountColumns.FLAGS, oldFlags);
434                    resolver.update(accountUri, accountValues, null, null);
435                    LogUtils.w(Logging.LOG_TAG, "[Incomplete flag cleared]");
436                }
437            }
438        } finally {
439            c.close();
440        }
441    }
442
443    /**
444     * Parse services.xml file to find our available email services
445     */
446    @SuppressWarnings("unchecked")
447    private static synchronized void findServices(final Context context) {
448        if (sServiceMap != null) {
449            return;
450        }
451
452        final ImmutableMap.Builder<String, EmailServiceInfo> builder = ImmutableMap.builder();
453
454        try {
455            final Resources res = context.getResources();
456            final XmlResourceParser xml = res.getXml(R.xml.services);
457            int xmlEventType;
458            // walk through senders.xml file.
459            while ((xmlEventType = xml.next()) != XmlResourceParser.END_DOCUMENT) {
460                if (xmlEventType == XmlResourceParser.START_TAG &&
461                        "emailservice".equals(xml.getName())) {
462                    final EmailServiceInfo info = new EmailServiceInfo();
463                    final TypedArray ta = res.obtainAttributes(xml, R.styleable.EmailServiceInfo);
464                    info.protocol = ta.getString(R.styleable.EmailServiceInfo_protocol);
465                    info.accountType = ta.getString(R.styleable.EmailServiceInfo_accountType);
466                    info.name = ta.getString(R.styleable.EmailServiceInfo_name);
467                    info.hide = ta.getBoolean(R.styleable.EmailServiceInfo_hide, false);
468                    final String klass = ta.getString(R.styleable.EmailServiceInfo_serviceClass);
469                    info.intentAction = ta.getString(R.styleable.EmailServiceInfo_intent);
470                    info.intentPackage = ta.getString(R.styleable.EmailServiceInfo_intentPackage);
471                    info.defaultSsl = ta.getBoolean(R.styleable.EmailServiceInfo_defaultSsl, false);
472                    info.port = ta.getInteger(R.styleable.EmailServiceInfo_port, 0);
473                    info.portSsl = ta.getInteger(R.styleable.EmailServiceInfo_portSsl, 0);
474                    info.offerTls = ta.getBoolean(R.styleable.EmailServiceInfo_offerTls, false);
475                    info.offerCerts = ta.getBoolean(R.styleable.EmailServiceInfo_offerCerts, false);
476                    info.offerLocalDeletes =
477                        ta.getBoolean(R.styleable.EmailServiceInfo_offerLocalDeletes, false);
478                    info.defaultLocalDeletes =
479                        ta.getInteger(R.styleable.EmailServiceInfo_defaultLocalDeletes,
480                                Account.DELETE_POLICY_ON_DELETE);
481                    info.offerPrefix =
482                        ta.getBoolean(R.styleable.EmailServiceInfo_offerPrefix, false);
483                    info.usesSmtp = ta.getBoolean(R.styleable.EmailServiceInfo_usesSmtp, false);
484                    info.usesAutodiscover =
485                        ta.getBoolean(R.styleable.EmailServiceInfo_usesAutodiscover, false);
486                    info.offerLookback =
487                        ta.getBoolean(R.styleable.EmailServiceInfo_offerLookback, false);
488                    info.defaultLookback =
489                        ta.getInteger(R.styleable.EmailServiceInfo_defaultLookback,
490                                SyncWindow.SYNC_WINDOW_3_DAYS);
491                    info.syncChanges =
492                        ta.getBoolean(R.styleable.EmailServiceInfo_syncChanges, false);
493                    info.syncContacts =
494                        ta.getBoolean(R.styleable.EmailServiceInfo_syncContacts, false);
495                    info.syncCalendar =
496                        ta.getBoolean(R.styleable.EmailServiceInfo_syncCalendar, false);
497                    info.offerAttachmentPreload =
498                        ta.getBoolean(R.styleable.EmailServiceInfo_offerAttachmentPreload, false);
499                    info.syncIntervalStrings =
500                        ta.getTextArray(R.styleable.EmailServiceInfo_syncIntervalStrings);
501                    info.syncIntervals =
502                        ta.getTextArray(R.styleable.EmailServiceInfo_syncIntervals);
503                    info.defaultSyncInterval =
504                        ta.getInteger(R.styleable.EmailServiceInfo_defaultSyncInterval, 15);
505                    info.inferPrefix = ta.getString(R.styleable.EmailServiceInfo_inferPrefix);
506                    info.offerLoadMore =
507                            ta.getBoolean(R.styleable.EmailServiceInfo_offerLoadMore, false);
508                    info.requiresSetup =
509                            ta.getBoolean(R.styleable.EmailServiceInfo_requiresSetup, false);
510
511                    // Must have either "class" (local) or "intent" (remote)
512                    if (klass != null) {
513                        try {
514                            info.klass = (Class<? extends Service>) Class.forName(klass);
515                        } catch (ClassNotFoundException e) {
516                            throw new IllegalStateException(
517                                    "Class not found in service descriptor: " + klass);
518                        }
519                    }
520                    if (info.klass == null && info.intentAction == null) {
521                        throw new IllegalStateException(
522                                "No class or intent action specified in service descriptor");
523                    }
524                    if (info.klass != null && info.intentAction != null) {
525                        throw new IllegalStateException(
526                                "Both class and intent action specified in service descriptor");
527                    }
528                    builder.put(info.protocol, info);
529                }
530            }
531        } catch (XmlPullParserException e) {
532            // ignore
533        } catch (IOException e) {
534            // ignore
535        }
536        sServiceMap = builder.build();
537    }
538
539    private static Uri asCalendarSyncAdapter(Uri uri, String account, String accountType) {
540        return uri.buildUpon().appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true")
541                .appendQueryParameter(Calendars.ACCOUNT_NAME, account)
542                .appendQueryParameter(Calendars.ACCOUNT_TYPE, accountType).build();
543    }
544
545    /**
546     * A no-op service that can be returned for non-existent/null protocols
547     */
548    class NullService implements IEmailService {
549        @Override
550        public IBinder asBinder() {
551            return null;
552        }
553
554        @Override
555        public Bundle validate(HostAuth hostauth) throws RemoteException {
556            return null;
557        }
558
559        @Override
560        public void startSync(long mailboxId, boolean userRequest, int deltaMessageCount)
561                throws RemoteException {
562        }
563
564        @Override
565        public void stopSync(long mailboxId) throws RemoteException {
566        }
567
568        @Override
569        public void loadMore(long messageId) throws RemoteException {
570        }
571
572        @Override
573        public void loadAttachment(final IEmailServiceCallback cb, final long attachmentId,
574                final boolean background) throws RemoteException {
575        }
576
577        @Override
578        public void updateFolderList(long accountId) throws RemoteException {
579        }
580
581        @Override
582        public boolean createFolder(long accountId, String name) throws RemoteException {
583            return false;
584        }
585
586        @Override
587        public boolean deleteFolder(long accountId, String name) throws RemoteException {
588            return false;
589        }
590
591        @Override
592        public boolean renameFolder(long accountId, String oldName, String newName)
593                throws RemoteException {
594            return false;
595        }
596
597        @Override
598        public void setLogging(int on) throws RemoteException {
599        }
600
601        @Override
602        public void hostChanged(long accountId) throws RemoteException {
603        }
604
605        @Override
606        public Bundle autoDiscover(String userName, String password) throws RemoteException {
607            return null;
608        }
609
610        @Override
611        public void sendMeetingResponse(long messageId, int response) throws RemoteException {
612        }
613
614        @Override
615        public void deleteAccountPIMData(final String emailAddress) throws RemoteException {
616        }
617
618        @Override
619        public int getApiLevel() throws RemoteException {
620            return Api.LEVEL;
621        }
622
623        @Override
624        public int searchMessages(long accountId, SearchParams params, long destMailboxId)
625                throws RemoteException {
626            return 0;
627        }
628
629        @Override
630        public void sendMail(long accountId) throws RemoteException {
631        }
632
633        @Override
634        public void serviceUpdated(String emailAddress) throws RemoteException {
635        }
636
637        @Override
638        public int getCapabilities(Account acct) throws RemoteException {
639            return 0;
640        }
641    }
642}
643