Home | History | Annotate | Download | only in eas
      1 /*
      2  * Copyright (C) 2013 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 
     17 package com.android.exchange.eas;
     18 
     19 import android.content.ContentResolver;
     20 import android.content.ContentUris;
     21 import android.content.ContentValues;
     22 import android.content.Context;
     23 import android.content.SyncResult;
     24 import android.net.Uri;
     25 import android.os.Build;
     26 import android.os.Bundle;
     27 import android.telephony.TelephonyManager;
     28 import android.text.TextUtils;
     29 import android.text.format.DateUtils;
     30 
     31 import com.android.emailcommon.provider.Account;
     32 import com.android.emailcommon.provider.EmailContent;
     33 import com.android.emailcommon.provider.HostAuth;
     34 import com.android.emailcommon.provider.Mailbox;
     35 import com.android.emailcommon.utility.Utility;
     36 import com.android.exchange.CommandStatusException;
     37 import com.android.exchange.Eas;
     38 import com.android.exchange.EasResponse;
     39 import com.android.exchange.adapter.Serializer;
     40 import com.android.exchange.adapter.Tags;
     41 import com.android.exchange.service.EasServerConnection;
     42 import com.android.mail.utils.LogUtils;
     43 
     44 import org.apache.http.HttpEntity;
     45 import org.apache.http.client.methods.HttpUriRequest;
     46 import org.apache.http.entity.ByteArrayEntity;
     47 
     48 import java.io.IOException;
     49 import java.security.cert.CertificateException;
     50 import java.util.ArrayList;
     51 
     52 /**
     53  * Base class for all Exchange operations that use a POST to talk to the server.
     54  *
     55  * The core of this class is {@link #performOperation}, which provides the skeleton of making
     56  * a request, handling common errors, and setting fields on the {@link SyncResult} if there is one.
     57  * This class abstracts the connection handling from its subclasses and callers.
     58  *
     59  * A subclass must implement the abstract functions below that create the request and parse the
     60  * response. There are also a set of functions that a subclass may override if it's substantially
     61  * different from the "normal" operation (e.g. most requests use the same request URI, but auto
     62  * discover deviates since it's not account-specific), but the default implementation should suffice
     63  * for most. The subclass must also define a public function which calls {@link #performOperation},
     64  * possibly doing nothing other than that. (I chose to force subclasses to do this, rather than
     65  * provide that function in the base class, in order to force subclasses to consider, for example,
     66  * whether it needs a {@link SyncResult} parameter, and what the proper name for the "doWork"
     67  * function ought to be for the subclass.)
     68  */
     69 public abstract class EasOperation {
     70     public static final String LOG_TAG = Eas.LOG_TAG;
     71 
     72     /** The maximum number of server redirects we allow before returning failure. */
     73     private static final int MAX_REDIRECTS = 3;
     74 
     75     /** Message MIME type for EAS version 14 and later. */
     76     private static final String EAS_14_MIME_TYPE = "application/vnd.ms-sync.wbxml";
     77 
     78     /** Error code indicating the operation was cancelled via {@link #abort}. */
     79     public static final int RESULT_ABORT = -1;
     80     /** Error code indicating the operation was cancelled via {@link #restart}. */
     81     public static final int RESULT_RESTART = -2;
     82     /** Error code indicating the Exchange servers redirected too many times. */
     83     public static final int RESULT_TOO_MANY_REDIRECTS = -3;
     84     /** Error code indicating the request failed due to a network problem. */
     85     public static final int RESULT_REQUEST_FAILURE = -4;
     86     /** Error code indicating a 403 (forbidden) error. */
     87     public static final int RESULT_FORBIDDEN = -5;
     88     /** Error code indicating an unresolved provisioning error. */
     89     public static final int RESULT_PROVISIONING_ERROR = -6;
     90     /** Error code indicating an authentication problem. */
     91     public static final int RESULT_AUTHENTICATION_ERROR = -7;
     92     /** Error code indicating the client is missing a certificate. */
     93     public static final int RESULT_CLIENT_CERTIFICATE_REQUIRED = -8;
     94     /** Error code indicating we don't have a protocol version in common with the server. */
     95     public static final int RESULT_PROTOCOL_VERSION_UNSUPPORTED = -9;
     96     /** Error code indicating some other failure. */
     97     public static final int RESULT_OTHER_FAILURE = -10;
     98 
     99     protected final Context mContext;
    100 
    101     /**
    102      * The account id for this operation.
    103      * NOTE: You will be tempted to add a reference to the {@link Account} here. Resist.
    104      * It's too easy for that to lead to creep and stale data.
    105      */
    106     protected final long mAccountId;
    107     private final EasServerConnection mConnection;
    108 
    109     // TODO: Make this private again when EasSyncHandler is converted to be a subclass.
    110     protected EasOperation(final Context context, final long accountId,
    111             final EasServerConnection connection) {
    112         mContext = context;
    113         mAccountId = accountId;
    114         mConnection = connection;
    115     }
    116 
    117     protected EasOperation(final Context context, final Account account, final HostAuth hostAuth) {
    118         this(context, account.mId, new EasServerConnection(context, account, hostAuth));
    119     }
    120 
    121     protected EasOperation(final Context context, final Account account) {
    122         this(context, account, HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv));
    123     }
    124 
    125     /**
    126      * This constructor is for use by operations that are created by other operations, e.g.
    127      * {@link EasProvision}.
    128      * @param parentOperation The {@link EasOperation} that is creating us.
    129      */
    130     protected EasOperation(final EasOperation parentOperation) {
    131         this(parentOperation.mContext, parentOperation.mAccountId, parentOperation.mConnection);
    132     }
    133 
    134     /**
    135      * Request that this operation terminate. Intended for use by the sync service to interrupt
    136      * running operations, primarily Ping.
    137      */
    138     public final void abort() {
    139         mConnection.stop(EasServerConnection.STOPPED_REASON_ABORT);
    140     }
    141 
    142     /**
    143      * Request that this operation restart. Intended for use by the sync service to interrupt
    144      * running operations, primarily Ping.
    145      */
    146     public final void restart() {
    147         mConnection.stop(EasServerConnection.STOPPED_REASON_RESTART);
    148     }
    149 
    150     /**
    151      * The skeleton of performing an operation. This function handles all the common code and
    152      * error handling, calling into virtual functions that are implemented or overridden by the
    153      * subclass to do the operation-specific logic.
    154      *
    155      * The result codes work as follows:
    156      * - Negative values indicate common error codes and are defined above (the various RESULT_*
    157      *   constants).
    158      * - Non-negative values indicate the result of {@link #handleResponse}. These are obviously
    159      *   specific to the subclass, and may indicate success or error conditions.
    160      *
    161      * The common error codes primarily indicate conditions that occur when performing the POST
    162      * itself, such as network errors and handling of the HTTP response. However, some errors that
    163      * can be indicated in the HTTP response code can also be indicated in the payload of the
    164      * response as well, so {@link #handleResponse} should in those cases return the appropriate
    165      * negative result code, which will be handled the same as if it had been indicated in the HTTP
    166      * response code.
    167      *
    168      * @param syncResult If this operation is a sync, the {@link SyncResult} object that should
    169      *                   be written to for this sync; otherwise null.
    170      * @return A result code for the outcome of this operation, as described above.
    171      */
    172     protected final int performOperation(final SyncResult syncResult) {
    173         // We handle server redirects by looping, but we need to protect against too much looping.
    174         int redirectCount = 0;
    175 
    176         do {
    177             // Perform the HTTP request and handle exceptions.
    178             final EasResponse response;
    179             try {
    180                 response = mConnection.executeHttpUriRequest(makeRequest(), getTimeout());
    181             } catch (final IOException e) {
    182                 // If we were stopped, return the appropriate result code.
    183                 switch (mConnection.getStoppedReason()) {
    184                     case EasServerConnection.STOPPED_REASON_ABORT:
    185                         return RESULT_ABORT;
    186                     case EasServerConnection.STOPPED_REASON_RESTART:
    187                         return RESULT_RESTART;
    188                     default:
    189                         break;
    190                 }
    191                 // If we're here, then we had a IOException that's not from a stop request.
    192                 String message = e.getMessage();
    193                 if (message == null) {
    194                     message = "(no message)";
    195                 }
    196                 LogUtils.i(LOG_TAG, "IOException while sending request: %s", message);
    197                 if (syncResult != null) {
    198                     ++syncResult.stats.numIoExceptions;
    199                 }
    200                 return RESULT_REQUEST_FAILURE;
    201             } catch (final CertificateException e) {
    202                 LogUtils.i(LOG_TAG, "CertificateException while sending request: %s",
    203                         e.getMessage());
    204                 if (syncResult != null) {
    205                     // TODO: Is this the best stat to increment?
    206                     ++syncResult.stats.numAuthExceptions;
    207                 }
    208                 return RESULT_CLIENT_CERTIFICATE_REQUIRED;
    209             } catch (final IllegalStateException e) {
    210                 // Subclasses use ISE to signal a hard error when building the request.
    211                 // TODO: Switch away from ISEs.
    212                 LogUtils.e(LOG_TAG, e, "Exception while sending request");
    213                 if (syncResult != null) {
    214                     syncResult.databaseError = true;
    215                 }
    216                 return RESULT_OTHER_FAILURE;
    217             }
    218 
    219             // The POST completed, so process the response.
    220             try {
    221                 final int result;
    222                 // First off, the success case.
    223                 if (response.isSuccess()) {
    224                     int responseResult;
    225                     try {
    226                         responseResult = handleResponse(response, syncResult);
    227                     } catch (final IOException e) {
    228                         LogUtils.e(LOG_TAG, e, "Exception while handling response");
    229                         if (syncResult != null) {
    230                             ++syncResult.stats.numIoExceptions;
    231                         }
    232                         return RESULT_REQUEST_FAILURE;
    233                     } catch (final CommandStatusException e) {
    234                         // For some operations (notably Sync & FolderSync), errors are signaled in
    235                         // the payload of the response. These will have a HTTP 200 response, and the
    236                         // error condition is only detected during response parsing.
    237                         // The various parsers handle this by throwing a CommandStatusException.
    238                         // TODO: Consider having the parsers return the errors instead of throwing.
    239                         final int status = e.mStatus;
    240                         LogUtils.e(LOG_TAG, "CommandStatusException: %s, %d", getCommand(), status);
    241                         if (CommandStatusException.CommandStatus.isNeedsProvisioning(status)) {
    242                             responseResult = RESULT_PROVISIONING_ERROR;
    243                         } else if (CommandStatusException.CommandStatus.isDeniedAccess(status)) {
    244                             responseResult = RESULT_FORBIDDEN;
    245                         } else {
    246                             responseResult = RESULT_OTHER_FAILURE;
    247                         }
    248                     }
    249                     result = responseResult;
    250                 } else {
    251                     result = RESULT_OTHER_FAILURE;
    252                 }
    253 
    254                 // Non-negative results indicate success. Return immediately and bypass the error
    255                 // handling.
    256                 if (result >= 0) {
    257                     return result;
    258                 }
    259 
    260                 // If this operation has distinct handling for 403 errors, do that.
    261                 if (result == RESULT_FORBIDDEN || (response.isForbidden() && handleForbidden())) {
    262                     LogUtils.e(LOG_TAG, "Forbidden response");
    263                     if (syncResult != null) {
    264                         // TODO: Is this the best stat to increment?
    265                         ++syncResult.stats.numAuthExceptions;
    266                     }
    267                     return RESULT_FORBIDDEN;
    268                 }
    269 
    270                 // Handle provisioning errors.
    271                 if (result == RESULT_PROVISIONING_ERROR || response.isProvisionError()) {
    272                     if (handleProvisionError(syncResult, mAccountId)) {
    273                         // The provisioning error has been taken care of, so we should re-do this
    274                         // request.
    275                         LogUtils.d(LOG_TAG, "Provisioning error handled during %s, retrying",
    276                                 getCommand());
    277                         continue;
    278                     }
    279                     if (syncResult != null) {
    280                         LogUtils.e(LOG_TAG, "Issue with provisioning");
    281                         // TODO: Is this the best stat to increment?
    282                         ++syncResult.stats.numAuthExceptions;
    283                     }
    284                     return RESULT_PROVISIONING_ERROR;
    285                 }
    286 
    287                 // Handle authentication errors.
    288                 if (response.isAuthError()) {
    289                     LogUtils.e(LOG_TAG, "Authentication error");
    290                     if (syncResult != null) {
    291                         ++syncResult.stats.numAuthExceptions;
    292                     }
    293                     if (response.isMissingCertificate()) {
    294                         return RESULT_CLIENT_CERTIFICATE_REQUIRED;
    295                     }
    296                     return RESULT_AUTHENTICATION_ERROR;
    297                 }
    298 
    299                 // Handle redirects.
    300                 if (response.isRedirectError()) {
    301                     ++redirectCount;
    302                     mConnection.redirectHostAuth(response.getRedirectAddress());
    303                     // Note that unlike other errors, we do NOT return here; we just keep looping.
    304                 } else {
    305                     // All other errors.
    306                     LogUtils.e(LOG_TAG, "Generic error for operation %s: status %d, result %d",
    307                             getCommand(), response.getStatus(), result);
    308                     if (syncResult != null) {
    309                         // TODO: Is this the best stat to increment?
    310                         ++syncResult.stats.numIoExceptions;
    311                     }
    312                     return RESULT_OTHER_FAILURE;
    313                 }
    314             } finally {
    315                 response.close();
    316             }
    317         } while (redirectCount < MAX_REDIRECTS);
    318 
    319         // Non-redirects return immediately after handling, so the only way to reach here is if we
    320         // looped too many times.
    321         LogUtils.e(LOG_TAG, "Too many redirects");
    322         if (syncResult != null) {
    323            syncResult.tooManyRetries = true;
    324         }
    325         return RESULT_TOO_MANY_REDIRECTS;
    326     }
    327 
    328     /**
    329      * Reset the protocol version to use for this connection. If it's changed, and our account is
    330      * persisted, also write back the changes to the DB.
    331      * @param protocolVersion The new protocol version to use, as a string.
    332      */
    333     protected final void setProtocolVersion(final String protocolVersion) {
    334         if (mConnection.setProtocolVersion(protocolVersion) && mAccountId != Account.NOT_SAVED) {
    335             final Uri uri = ContentUris.withAppendedId(Account.CONTENT_URI, mAccountId);
    336             final ContentValues cv = new ContentValues(2);
    337             if (getProtocolVersion() >= 12.0) {
    338                 final int oldFlags = Utility.getFirstRowInt(mContext, uri,
    339                         Account.ACCOUNT_FLAGS_PROJECTION, null, null, null,
    340                         Account.ACCOUNT_FLAGS_COLUMN_FLAGS, 0);
    341                 final int newFlags = oldFlags
    342                         | Account.FLAGS_SUPPORTS_GLOBAL_SEARCH + Account.FLAGS_SUPPORTS_SEARCH;
    343                 if (oldFlags != newFlags) {
    344                     cv.put(EmailContent.AccountColumns.FLAGS, newFlags);
    345                 }
    346             }
    347             cv.put(EmailContent.AccountColumns.PROTOCOL_VERSION, protocolVersion);
    348             mContext.getContentResolver().update(uri, cv, null, null);
    349         }
    350     }
    351 
    352     /**
    353      * Create the request object for this operation.
    354      * Most operations use a POST, but some use other request types (e.g. Options).
    355      * @return An {@link HttpUriRequest}.
    356      * @throws IOException
    357      */
    358     private final HttpUriRequest makeRequest() throws IOException {
    359         final String requestUri = getRequestUri();
    360         if (requestUri == null) {
    361             return mConnection.makeOptions();
    362         }
    363         return mConnection.makePost(requestUri, getRequestEntity(),
    364                 getRequestContentType(), addPolicyKeyHeaderToRequest());
    365     }
    366 
    367     /**
    368      * The following functions MUST be overridden by subclasses; these are things that are unique
    369      * to each operation.
    370      */
    371 
    372     /**
    373      * Get the name of the operation, used as the "Cmd=XXX" query param in the request URI. Note
    374      * that if you override {@link #getRequestUri}, then this function may be unused for normal
    375      * operation, but all subclasses should return something non-null for use with logging.
    376      * @return The name of the command for this operation as defined by the EAS protocol, or for
    377      *         commands that don't need it, a suitable descriptive name for logging.
    378      */
    379     protected abstract String getCommand();
    380 
    381     /**
    382      * Build the {@link HttpEntity} which is used to construct the POST. Typically this function
    383      * will build the Exchange request using a {@link Serializer} and then call {@link #makeEntity}.
    384      * If the subclass is not using a POST, then it should override this to return null.
    385      * @return The {@link HttpEntity} to pass to {@link EasServerConnection#makePost}.
    386      * @throws IOException
    387      */
    388     protected abstract HttpEntity getRequestEntity() throws IOException;
    389 
    390     /**
    391      * Parse the response from the Exchange perform whatever actions are dictated by that.
    392      * @param response The {@link EasResponse} to our request.
    393      * @param syncResult The {@link SyncResult} object for this operation, or null if we're not
    394      *                   handling a sync.
    395      * @return A result code. Non-negative values are returned directly to the caller; negative
    396      *         values
    397      *
    398      * that is returned to the caller of {@link #performOperation}.
    399      * @throws IOException
    400      */
    401     protected abstract int handleResponse(final EasResponse response, final SyncResult syncResult)
    402             throws IOException, CommandStatusException;
    403 
    404     /**
    405      * The following functions may be overriden by a subclass, but most operations will not need
    406      * to do so.
    407      */
    408 
    409     /**
    410      * Get the URI for the Exchange server and this operation. Most (signed in) operations need
    411      * not override this; the notable operation that needs to override it is auto-discover.
    412      * @return
    413      */
    414     protected String getRequestUri() {
    415         return mConnection.makeUriString(getCommand());
    416     }
    417 
    418     /**
    419      * @return Whether to set the X-MS-PolicyKey header. Only Ping does not want this header.
    420      */
    421     protected boolean addPolicyKeyHeaderToRequest() {
    422         return true;
    423     }
    424 
    425     /**
    426      * @return The content type of this request.
    427      */
    428     protected String getRequestContentType() {
    429         return EAS_14_MIME_TYPE;
    430     }
    431 
    432     /**
    433      * @return The timeout to use for the POST.
    434      */
    435     protected long getTimeout() {
    436         return 30 * DateUtils.SECOND_IN_MILLIS;
    437     }
    438 
    439     /**
    440      * If 403 responses should be handled in a special way, this function should be overridden to
    441      * do that.
    442      * @return Whether we handle 403 responses; if false, then treat 403 as a provisioning error.
    443      */
    444     protected boolean handleForbidden() {
    445         return false;
    446     }
    447 
    448     /**
    449      * Handle a provisioning error. Subclasses may override this to do something different, e.g.
    450      * to validate rather than actually do the provisioning.
    451      * @param syncResult
    452      * @param accountId
    453      * @return
    454      */
    455     protected boolean handleProvisionError(final SyncResult syncResult, final long accountId) {
    456         final EasProvision provisionOperation = new EasProvision(this);
    457         return provisionOperation.provision(syncResult, accountId);
    458     }
    459 
    460     /**
    461      * Convenience methods for subclasses to use.
    462      */
    463 
    464     /**
    465      * Convenience method to make an {@link HttpEntity} from {@link Serializer}.
    466      */
    467     protected final HttpEntity makeEntity(final Serializer s) {
    468         return new ByteArrayEntity(s.toByteArray());
    469     }
    470 
    471     /**
    472      * Check whether we should ask the server what protocol versions it supports and set this
    473      * account to use that version.
    474      * @return Whether we need a new protocol version from the server.
    475      */
    476     protected final boolean shouldGetProtocolVersion() {
    477         // TODO: Find conditions under which we should check other than not having one yet.
    478         return !mConnection.isProtocolVersionSet();
    479     }
    480 
    481     /**
    482      * @return The protocol version to use.
    483      */
    484     protected final double getProtocolVersion() {
    485         return mConnection.getProtocolVersion();
    486     }
    487 
    488     /**
    489      * @return Our useragent.
    490      */
    491     protected final String getUserAgent() {
    492         return mConnection.getUserAgent();
    493     }
    494 
    495     /**
    496      * @return Whether we succeeeded in registering the client cert.
    497      */
    498     protected final boolean registerClientCert() {
    499         return mConnection.registerClientCert();
    500     }
    501 
    502     /**
    503      * Add the device information to the current request.
    504      * @param s The {@link Serializer} for our current request.
    505      * @throws IOException
    506      */
    507     protected final void addDeviceInformationToSerlializer(final Serializer s) throws IOException {
    508         final TelephonyManager tm = (TelephonyManager)mContext.getSystemService(
    509                 Context.TELEPHONY_SERVICE);
    510         final String deviceId;
    511         final String phoneNumber;
    512         final String operator;
    513         if (tm != null) {
    514             deviceId = tm.getDeviceId();
    515             phoneNumber = tm.getLine1Number();
    516             // TODO: This is not perfect and needs to be improved, for at least two reasons:
    517             // 1) SIM cards can override this name.
    518             // 2) We don't resend this info to the server when we change networks.
    519             final String operatorName = tm.getNetworkOperatorName();
    520             final String operatorNumber = tm.getNetworkOperator();
    521             if (!TextUtils.isEmpty(operatorName) && !TextUtils.isEmpty(operatorNumber)) {
    522                 operator = operatorName + " (" + operatorNumber + ")";
    523             } else if (!TextUtils.isEmpty(operatorName)) {
    524                 operator = operatorName;
    525             } else {
    526                 operator = operatorNumber;
    527             }
    528         } else {
    529             deviceId = null;
    530             phoneNumber = null;
    531             operator = null;
    532         }
    533 
    534         // TODO: Right now, we won't send this information unless the device is provisioned again.
    535         // Potentially, this means that our phone number could be out of date if the user
    536         // switches sims. Is there something we can do to force a reprovision?
    537         s.start(Tags.SETTINGS_DEVICE_INFORMATION).start(Tags.SETTINGS_SET);
    538         s.data(Tags.SETTINGS_MODEL, Build.MODEL);
    539         if (deviceId != null) {
    540             s.data(Tags.SETTINGS_IMEI, tm.getDeviceId());
    541         }
    542         // Set the device friendly name, if we have one.
    543         // TODO: Longer term, this should be done without a provider call.
    544         final Bundle bundle = mContext.getContentResolver().call(
    545                 EmailContent.CONTENT_URI, EmailContent.DEVICE_FRIENDLY_NAME, null, null);
    546         if (bundle != null) {
    547             final String friendlyName = bundle.getString(EmailContent.DEVICE_FRIENDLY_NAME);
    548             if (!TextUtils.isEmpty(friendlyName)) {
    549                 s.data(Tags.SETTINGS_FRIENDLY_NAME, friendlyName);
    550             }
    551         }
    552         s.data(Tags.SETTINGS_OS, "Android " + Build.VERSION.RELEASE);
    553         if (phoneNumber != null) {
    554             s.data(Tags.SETTINGS_PHONE_NUMBER, phoneNumber);
    555         }
    556         // TODO: Consider setting this, but make sure we know what it's used for.
    557         // If the user changes the device's locale and we don't do a reprovision, the server's
    558         // idea of the language will be wrong. Since we're not sure what this is used for,
    559         // right now we're leaving it out.
    560         //s.data(Tags.SETTINGS_OS_LANGUAGE, Locale.getDefault().getDisplayLanguage());
    561         s.data(Tags.SETTINGS_USER_AGENT, getUserAgent());
    562         if (operator != null) {
    563             s.data(Tags.SETTINGS_MOBILE_OPERATOR, operator);
    564         }
    565         s.end().end();  // SETTINGS_SET, SETTINGS_DEVICE_INFORMATION
    566     }
    567 
    568     /**
    569      * Convenience method for adding a Message to an account's outbox
    570      * @param account The {@link Account} from which to send the message.
    571      * @param msg the message to send
    572      */
    573     protected final void sendMessage(final Account account, final EmailContent.Message msg) {
    574         long mailboxId = Mailbox.findMailboxOfType(mContext, account.mId, Mailbox.TYPE_OUTBOX);
    575         // TODO: Improve system mailbox handling.
    576         if (mailboxId == Mailbox.NO_MAILBOX) {
    577             LogUtils.d(LOG_TAG, "No outbox for account %d, creating it", account.mId);
    578             final Mailbox outbox =
    579                     Mailbox.newSystemMailbox(mContext, account.mId, Mailbox.TYPE_OUTBOX);
    580             outbox.save(mContext);
    581             mailboxId = outbox.mId;
    582         }
    583         msg.mMailboxKey = mailboxId;
    584         msg.mAccountKey = account.mId;
    585         msg.save(mContext);
    586         requestSyncForMailbox(new android.accounts.Account(account.mEmailAddress,
    587                 Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), mailboxId);
    588     }
    589 
    590     /**
    591      * Issue a {@link android.content.ContentResolver#requestSync} for a specific mailbox.
    592      * @param amAccount The {@link android.accounts.Account} for the account we're pinging.
    593      * @param mailboxId The id of the mailbox that needs to sync.
    594      */
    595     protected static void requestSyncForMailbox(final android.accounts.Account amAccount,
    596             final long mailboxId) {
    597         final Bundle extras = Mailbox.createSyncBundle(mailboxId);
    598         ContentResolver.requestSync(amAccount, EmailContent.AUTHORITY, extras);
    599         LogUtils.i(LOG_TAG, "requestSync EasOperation requestSyncForMailbox %s, %s",
    600                 amAccount.toString(), extras.toString());
    601     }
    602 
    603     protected static void requestSyncForMailboxes(final android.accounts.Account amAccount,
    604             final ArrayList<Long> mailboxIds) {
    605         final Bundle extras = Mailbox.createSyncBundle(mailboxIds);
    606         ContentResolver.requestSync(amAccount, EmailContent.AUTHORITY, extras);
    607         LogUtils.i(LOG_TAG, "requestSync EasOperation requestSyncForMailboxes  %s, %s",
    608                 amAccount.toString(), extras.toString());
    609     }
    610 
    611     /**
    612      * RequestNoOpSync
    613      * This requests a sync for a particular authority purely so that that account
    614      * in settings will recognize that it is trying to sync, and will display the
    615      * appropriate UI. In fact, all exchange data syncing actually happens through the
    616      * EmailSyncAdapterService.
    617      * @param amAccount
    618      * @param authority
    619      */
    620     protected static void requestNoOpSync(final android.accounts.Account amAccount,
    621             final String authority) {
    622         final Bundle extras = new Bundle(1);
    623         extras.putBoolean(Mailbox.SYNC_EXTRA_NOOP, true);
    624         ContentResolver.requestSync(amAccount, authority, extras);
    625         LogUtils.d(LOG_TAG, "requestSync EasOperation requestNoOpSync %s, %s",
    626                 amAccount.toString(), extras.toString());
    627     }
    628 }
    629