Home | History | Annotate | Download | only in exchange
      1 /*
      2  * Copyright (C) 2008-2009 Marc Blank
      3  * Licensed to The Android Open Source Project.
      4  *
      5  * Licensed under the Apache License, Version 2.0 (the "License");
      6  * you may not use this file except in compliance with the License.
      7  * You may obtain a copy of the License at
      8  *
      9  *      http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  * Unless required by applicable law or agreed to in writing, software
     12  * distributed under the License is distributed on an "AS IS" BASIS,
     13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  * See the License for the specific language governing permissions and
     15  * limitations under the License.
     16  */
     17 
     18 package com.android.exchange;
     19 
     20 import android.content.ContentResolver;
     21 import android.content.ContentUris;
     22 import android.content.ContentValues;
     23 import android.content.Context;
     24 import android.content.Entity;
     25 import android.database.Cursor;
     26 import android.net.TrafficStats;
     27 import android.net.Uri;
     28 import android.os.Build;
     29 import android.os.Bundle;
     30 import android.os.RemoteException;
     31 import android.provider.CalendarContract.Attendees;
     32 import android.provider.CalendarContract.Events;
     33 import android.text.TextUtils;
     34 import android.text.format.DateUtils;
     35 import android.util.Base64;
     36 import android.util.Xml;
     37 
     38 import com.android.emailcommon.TrafficFlags;
     39 import com.android.emailcommon.mail.Address;
     40 import com.android.emailcommon.mail.MeetingInfo;
     41 import com.android.emailcommon.mail.MessagingException;
     42 import com.android.emailcommon.mail.PackedString;
     43 import com.android.emailcommon.provider.Account;
     44 import com.android.emailcommon.provider.EmailContent.AccountColumns;
     45 import com.android.emailcommon.provider.EmailContent.Message;
     46 import com.android.emailcommon.provider.EmailContent.MessageColumns;
     47 import com.android.emailcommon.provider.EmailContent.SyncColumns;
     48 import com.android.emailcommon.provider.HostAuth;
     49 import com.android.emailcommon.provider.Mailbox;
     50 import com.android.emailcommon.provider.Policy;
     51 import com.android.emailcommon.provider.ProviderUnavailableException;
     52 import com.android.emailcommon.service.EmailServiceConstants;
     53 import com.android.emailcommon.service.EmailServiceProxy;
     54 import com.android.emailcommon.service.EmailServiceStatus;
     55 import com.android.emailcommon.service.PolicyServiceProxy;
     56 import com.android.emailcommon.utility.EmailClientConnectionManager;
     57 import com.android.emailcommon.utility.Utility;
     58 import com.android.emailsync.AbstractSyncService;
     59 import com.android.emailsync.PartRequest;
     60 import com.android.emailsync.Request;
     61 import com.android.exchange.CommandStatusException.CommandStatus;
     62 import com.android.exchange.adapter.AbstractSyncAdapter;
     63 import com.android.exchange.adapter.AccountSyncAdapter;
     64 import com.android.exchange.adapter.AttachmentLoader;
     65 import com.android.exchange.adapter.EmailSyncAdapter;
     66 import com.android.exchange.adapter.FolderSyncParser;
     67 import com.android.exchange.adapter.GalParser;
     68 import com.android.exchange.adapter.MeetingResponseParser;
     69 import com.android.exchange.adapter.MoveItemsParser;
     70 import com.android.exchange.adapter.Parser.EmptyStreamException;
     71 import com.android.exchange.adapter.ProvisionParser;
     72 import com.android.exchange.adapter.Serializer;
     73 import com.android.exchange.adapter.SettingsParser;
     74 import com.android.exchange.adapter.Tags;
     75 import com.android.exchange.provider.GalResult;
     76 import com.android.exchange.utility.CalendarUtilities;
     77 import com.android.exchange.utility.CurlLogger;
     78 import com.android.mail.utils.LogUtils;
     79 import com.google.common.annotations.VisibleForTesting;
     80 
     81 import org.apache.http.Header;
     82 import org.apache.http.HttpEntity;
     83 import org.apache.http.HttpResponse;
     84 import org.apache.http.HttpStatus;
     85 import org.apache.http.client.HttpClient;
     86 import org.apache.http.client.methods.HttpOptions;
     87 import org.apache.http.client.methods.HttpPost;
     88 import org.apache.http.client.methods.HttpRequestBase;
     89 import org.apache.http.entity.ByteArrayEntity;
     90 import org.apache.http.entity.StringEntity;
     91 import org.apache.http.impl.client.DefaultHttpClient;
     92 import org.apache.http.params.BasicHttpParams;
     93 import org.apache.http.params.HttpConnectionParams;
     94 import org.apache.http.params.HttpParams;
     95 import org.apache.http.protocol.BasicHttpProcessor;
     96 import org.xmlpull.v1.XmlPullParser;
     97 import org.xmlpull.v1.XmlPullParserException;
     98 import org.xmlpull.v1.XmlPullParserFactory;
     99 import org.xmlpull.v1.XmlSerializer;
    100 
    101 import java.io.ByteArrayOutputStream;
    102 import java.io.IOException;
    103 import java.io.InputStream;
    104 import java.lang.Thread.State;
    105 import java.net.URI;
    106 import java.security.cert.CertificateException;
    107 
    108 public class EasSyncService extends AbstractSyncService {
    109     // DO NOT CHECK IN SET TO TRUE
    110     public static final boolean DEBUG_GAL_SERVICE = false;
    111 
    112     protected static final String PING_COMMAND = "Ping";
    113     // Command timeout is the the time allowed for reading data from an open connection before an
    114     // IOException is thrown.  After a small added allowance, our watchdog alarm goes off (allowing
    115     // us to detect a silently dropped connection).  The allowance is defined below.
    116     static public final int COMMAND_TIMEOUT = (int)(30 * DateUtils.SECOND_IN_MILLIS);
    117     // Connection timeout is the time given to connect to the server before reporting an IOException
    118     static private final int CONNECTION_TIMEOUT = (int)(20 * DateUtils.SECOND_IN_MILLIS);
    119     // The extra time allowed beyond the COMMAND_TIMEOUT before which our watchdog alarm triggers
    120     static private final int WATCHDOG_TIMEOUT_ALLOWANCE = (int)(30 * DateUtils.SECOND_IN_MILLIS);
    121 
    122     static private final String AUTO_DISCOVER_SCHEMA_PREFIX =
    123         "http://schemas.microsoft.com/exchange/autodiscover/mobilesync/";
    124     static private final String AUTO_DISCOVER_PAGE = "/autodiscover/autodiscover.xml";
    125     static protected final int EAS_REDIRECT_CODE = 451;
    126 
    127     static public final int INTERNAL_SERVER_ERROR_CODE = 500;
    128 
    129     static public final String EAS_12_POLICY_TYPE = "MS-EAS-Provisioning-WBXML";
    130     static public final String EAS_2_POLICY_TYPE = "MS-WAP-Provisioning-XML";
    131 
    132     static public final int MESSAGE_FLAG_MOVED_MESSAGE = 1 << Message.FLAG_SYNC_ADAPTER_SHIFT;
    133     // The amount of time we allow for a thread to release its post lock after receiving an alert
    134     static private final int POST_LOCK_TIMEOUT = (int)(10 * DateUtils.SECOND_IN_MILLIS);
    135 
    136     // The EAS protocol Provision status for "we implement all of the policies"
    137     static private final String PROVISION_STATUS_OK = "1";
    138     // The EAS protocol Provision status meaning "we partially implement the policies"
    139     static private final String PROVISION_STATUS_PARTIAL = "2";
    140 
    141     static /*package*/ final String DEVICE_TYPE = "Android";
    142     static final String USER_AGENT = DEVICE_TYPE + '/' + Build.VERSION.RELEASE + '-' +
    143         Eas.CLIENT_VERSION;
    144 
    145     // Maximum number of times we'll allow a sync to "loop" with MoreAvailable true before
    146     // forcing it to stop.  This number has been determined empirically.
    147     static private final int MAX_LOOPING_COUNT = 100;
    148     // Reasonable default
    149     public String mProtocolVersion = Eas.DEFAULT_PROTOCOL_VERSION;
    150     public Double mProtocolVersionDouble;
    151     protected String mDeviceId = null;
    152     @VisibleForTesting
    153     String mAuthString = null;
    154     @VisibleForTesting
    155     String mUserString = null;
    156     @VisibleForTesting
    157     String mBaseUriString = null;
    158     public String mHostAddress;
    159     public String mUserName;
    160     public String mPassword;
    161 
    162     // The HttpPost in progress
    163     private volatile HttpPost mPendingPost = null;
    164     // Whether a POST was aborted due to alarm (watchdog alarm)
    165     protected boolean mPostAborted = false;
    166     // Whether a POST was aborted due to reset
    167     protected boolean mPostReset = false;
    168 
    169     // The parameters for the connection must be modified through setConnectionParameters
    170     private HostAuth mHostAuth;
    171     private boolean mSsl = true;
    172     private boolean mTrustSsl = false;
    173     private String mClientCertAlias = null;
    174 
    175     public ContentResolver mContentResolver;
    176     // Whether or not the sync service is valid (usable)
    177     public boolean mIsValid = true;
    178 
    179     // Whether the most recent upsync failed (status 7)
    180     public boolean mUpsyncFailed = false;
    181 
    182     protected EasSyncService(Context _context, Mailbox _mailbox) {
    183         super(_context, _mailbox);
    184         mContentResolver = _context.getContentResolver();
    185         if (mAccount == null) {
    186             mIsValid = false;
    187             return;
    188         }
    189         HostAuth ha = HostAuth.restoreHostAuthWithId(_context, mAccount.mHostAuthKeyRecv);
    190         if (ha == null) {
    191             mIsValid = false;
    192             return;
    193         }
    194         mSsl = (ha.mFlags & HostAuth.FLAG_SSL) != 0;
    195         mTrustSsl = (ha.mFlags & HostAuth.FLAG_TRUST_ALL) != 0;
    196     }
    197 
    198     private EasSyncService(String prefix) {
    199         super(prefix);
    200     }
    201 
    202     public EasSyncService() {
    203         this("EAS Validation");
    204     }
    205 
    206     public static EasSyncService getServiceForMailbox(Context context, Mailbox m) {
    207         switch(m.mType) {
    208             case Mailbox.TYPE_EAS_ACCOUNT_MAILBOX:
    209                 return new EasAccountService(context, m);
    210             case Mailbox.TYPE_OUTBOX:
    211                 return new EasOutboxService(context, m);
    212             default:
    213                 return new EasSyncService(context, m);
    214         }
    215     }
    216 
    217     @Override
    218     public void resetCalendarSyncKey() {
    219         // resetCalendarSyncKey is declared in AbstractSyncAdapterService,
    220         // so we need to define this function, by since CalendarSyncAdapter no longer exists,
    221         // we need to remove this:
    222 //        CalendarSyncAdapter adapter = new CalendarSyncAdapter(this);
    223 //            try {
    224 //                adapter.setSyncKey("0", false);
    225 //            } catch (IOException e) {
    226 //                // The provider can't be reached; nothing to be done
    227 //            }
    228 
    229      // For reference, this is what CalendarSyncAdapter.setSyncKey() did:
    230 //        synchronized (sSyncKeyLock) {
    231 //            if ("0".equals(syncKey) || !inCommands) {
    232 //                ContentProviderClient client = mService.mContentResolver
    233 //                        .acquireContentProviderClient(CalendarContract.CONTENT_URI);
    234 //                try {
    235 //                    SyncStateContract.Helpers.set(
    236 //                            client,
    237 //                            asSyncAdapter(SyncState.CONTENT_URI, mEmailAddress,
    238 //                                    Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), mAccountManagerAccount,
    239 //                            syncKey.getBytes());
    240 //                    userLog("SyncKey set to ", syncKey, " in CalendarProvider");
    241 //                } catch (RemoteException e) {
    242 //                    throw new IOException("Can't set SyncKey in CalendarProvider");
    243 //                }
    244 //            }
    245 //            mMailbox.mSyncKey = syncKey;
    246 //        }
    247     }
    248 
    249     /**
    250      * Try to wake up a sync thread that is waiting on an HttpClient POST and has waited past its
    251      * socket timeout without having thrown an Exception
    252      *
    253      * @return true if the POST was successfully stopped; false if we've failed and interrupted
    254      * the thread
    255      */
    256     @Override
    257     public boolean alarm() {
    258         HttpPost post;
    259         if (mThread == null) return true;
    260         String threadName = mThread.getName();
    261 
    262         // Synchronize here so that we are guaranteed to have valid mPendingPost and mPostLock
    263         // executePostWithTimeout (which executes the HttpPost) also uses this lock
    264         synchronized(getSynchronizer()) {
    265             // Get a reference to the current post lock
    266             post = mPendingPost;
    267             if (post != null) {
    268                 if (Eas.USER_LOG) {
    269                     URI uri = post.getURI();
    270                     if (uri != null) {
    271                         String query = uri.getQuery();
    272                         if (query == null) {
    273                             query = "POST";
    274                         }
    275                         userLog(threadName, ": Alert, aborting ", query);
    276                     } else {
    277                         userLog(threadName, ": Alert, no URI?");
    278                     }
    279                 }
    280                 // Abort the POST
    281                 mPostAborted = true;
    282                 post.abort();
    283             } else {
    284                 // If there's no POST, we're done
    285                 userLog("Alert, no pending POST");
    286                 return true;
    287             }
    288         }
    289 
    290         // Wait for the POST to finish
    291         try {
    292             Thread.sleep(POST_LOCK_TIMEOUT);
    293         } catch (InterruptedException e) {
    294         }
    295 
    296         State s = mThread.getState();
    297         if (Eas.USER_LOG) {
    298             userLog(threadName + ": State = " + s.name());
    299         }
    300 
    301         synchronized (getSynchronizer()) {
    302             // If the thread is still hanging around and the same post is pending, let's try to
    303             // stop the thread with an interrupt.
    304             if ((s != State.TERMINATED) && (mPendingPost != null) && (mPendingPost == post)) {
    305                 mStop = true;
    306                 mThread.interrupt();
    307                 userLog("Interrupting...");
    308                 // Let the caller know we had to interrupt the thread
    309                 return false;
    310             }
    311         }
    312         // Let the caller know that the alarm was handled normally
    313         return true;
    314     }
    315 
    316     @Override
    317     public void reset() {
    318         synchronized(getSynchronizer()) {
    319             if (mPendingPost != null) {
    320                 URI uri = mPendingPost.getURI();
    321                 if (uri != null) {
    322                     String query = uri.getQuery();
    323                     if (query.startsWith("Cmd=Ping")) {
    324                         userLog("Reset, aborting Ping");
    325                         mPostReset = true;
    326                         mPendingPost.abort();
    327                     }
    328                 }
    329             }
    330         }
    331     }
    332 
    333     @Override
    334     public void stop() {
    335         mStop = true;
    336         synchronized(getSynchronizer()) {
    337             if (mPendingPost != null) {
    338                 mPendingPost.abort();
    339             }
    340         }
    341     }
    342 
    343     void setupProtocolVersion(EasSyncService service, Header versionHeader)
    344             throws MessagingException {
    345         // The string is a comma separated list of EAS versions in ascending order
    346         // e.g. 1.0,2.0,2.5,12.0,12.1,14.0,14.1
    347         String supportedVersions = versionHeader.getValue();
    348         userLog("Server supports versions: ", supportedVersions);
    349         String[] supportedVersionsArray = supportedVersions.split(",");
    350         String ourVersion = null;
    351         // Find the most recent version we support
    352         for (String version: supportedVersionsArray) {
    353             if (version.equals(Eas.SUPPORTED_PROTOCOL_EX2003) ||
    354                     version.equals(Eas.SUPPORTED_PROTOCOL_EX2007) ||
    355                     version.equals(Eas.SUPPORTED_PROTOCOL_EX2007_SP1) ||
    356                     version.equals(Eas.SUPPORTED_PROTOCOL_EX2010) ||
    357                     version.equals(Eas.SUPPORTED_PROTOCOL_EX2010_SP1)) {
    358                 ourVersion = version;
    359             }
    360         }
    361         // If we don't support any of the servers supported versions, throw an exception here
    362         // This will cause validation to fail
    363         if (ourVersion == null) {
    364             LogUtils.w(TAG, "No supported EAS versions: " + supportedVersions);
    365             throw new MessagingException(MessagingException.PROTOCOL_VERSION_UNSUPPORTED);
    366         } else {
    367             // Debug code for testing EAS 14.0; disables support for EAS 14.1
    368             // "adb shell setprop log.tag.Exchange14 VERBOSE"
    369             if (ourVersion.equals(Eas.SUPPORTED_PROTOCOL_EX2010_SP1) &&
    370                     LogUtils.isLoggable("Exchange14", LogUtils.VERBOSE)) {
    371                 ourVersion = Eas.SUPPORTED_PROTOCOL_EX2010;
    372             }
    373             service.mProtocolVersion = ourVersion;
    374             service.mProtocolVersionDouble = Eas.getProtocolVersionDouble(ourVersion);
    375             Account account = service.mAccount;
    376             if (account != null) {
    377                 account.mProtocolVersion = ourVersion;
    378                 // Fixup search flags, if they're not set
    379                 if (service.mProtocolVersionDouble >= 12.0 &&
    380                         (account.mFlags & Account.FLAGS_SUPPORTS_SEARCH) == 0) {
    381                     if (account.isSaved()) {
    382                         ContentValues cv = new ContentValues();
    383                         account.mFlags |=
    384                             Account.FLAGS_SUPPORTS_GLOBAL_SEARCH + Account.FLAGS_SUPPORTS_SEARCH;
    385                         cv.put(AccountColumns.FLAGS, account.mFlags);
    386                         account.update(service.mContext, cv);
    387                     }
    388                 }
    389             }
    390         }
    391     }
    392 
    393     /**
    394      * Create an EasSyncService for the specified account
    395      *
    396      * @param context the caller's context
    397      * @param account the account
    398      * @return the service, or null if the account is on hold or hasn't been initialized
    399      */
    400     public static EasSyncService setupServiceForAccount(Context context, Account account) {
    401         // Just return null if we're on security hold
    402         if ((account.mFlags & Account.FLAGS_SECURITY_HOLD) != 0) {
    403             return null;
    404         }
    405         // If there's no protocol version, we're not initialized
    406         String protocolVersion = account.mProtocolVersion;
    407         if (protocolVersion == null) {
    408             return null;
    409         }
    410         EasSyncService svc = new EasSyncService("OutOfBand");
    411         HostAuth ha = HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv);
    412         svc.mProtocolVersion = protocolVersion;
    413         svc.mProtocolVersionDouble = Eas.getProtocolVersionDouble(protocolVersion);
    414         svc.mContext = context;
    415         svc.mHostAddress = ha.mAddress;
    416         svc.mUserName = ha.mLogin;
    417         svc.mPassword = ha.mPassword;
    418         try {
    419             svc.setConnectionParameters(ha);
    420             svc.mDeviceId = ExchangeService.getDeviceId(context);
    421         } catch (CertificateException e) {
    422             return null;
    423         }
    424         svc.mAccount = account;
    425         return svc;
    426     }
    427 
    428     /**
    429      * Get a redirect address and validate against it
    430      * @param resp the EasResponse to our POST
    431      * @param hostAuth the HostAuth we're using to validate
    432      * @return true if we have an updated HostAuth (with redirect address); false otherwise
    433      */
    434     protected boolean getValidateRedirect(EasResponse resp, HostAuth hostAuth) {
    435         Header locHeader = resp.getHeader("X-MS-Location");
    436         if (locHeader != null) {
    437             String loc;
    438             try {
    439                 loc = locHeader.getValue();
    440                 // Reset our host address and uncache our base uri
    441                 mHostAddress = Uri.parse(loc).getHost();
    442                 mBaseUriString = null;
    443                 hostAuth.mAddress = mHostAddress;
    444                 userLog("Redirecting to: " + loc);
    445                 return true;
    446             } catch (RuntimeException e) {
    447                 // Just don't crash if the Uri is illegal
    448             }
    449         }
    450         return false;
    451     }
    452 
    453     private static final int MAX_REDIRECTS = 3;
    454     private int mRedirectCount = 0;
    455 
    456     @Override
    457     public Bundle validateAccount(HostAuth hostAuth, Context context) {
    458         Bundle bundle = new Bundle();
    459         int resultCode = MessagingException.NO_ERROR;
    460         try {
    461             userLog("Testing EAS: ", hostAuth.mAddress, ", ", hostAuth.mLogin,
    462                     ", ssl = ", hostAuth.shouldUseSsl() ? "1" : "0");
    463             mContext = context;
    464             mHostAddress = hostAuth.mAddress;
    465             mUserName = hostAuth.mLogin;
    466             mPassword = hostAuth.mPassword;
    467 
    468             setConnectionParameters(hostAuth);
    469             mDeviceId = ExchangeService.getDeviceId(context);
    470             mAccount = new Account();
    471             mAccount.mEmailAddress = hostAuth.mLogin;
    472             EasResponse resp = sendHttpClientOptions();
    473             try {
    474                 int code = resp.getStatus();
    475                 userLog("Validation (OPTIONS) response: " + code);
    476                 if (code == HttpStatus.SC_OK) {
    477                     // No exception means successful validation
    478                     Header commands = resp.getHeader("MS-ASProtocolCommands");
    479                     Header versions = resp.getHeader("ms-asprotocolversions");
    480                     // Make sure we've got the right protocol version set up
    481                     try {
    482                         if (commands == null || versions == null) {
    483                             userLog("OPTIONS response without commands or versions");
    484                             // We'll treat this as a protocol exception
    485                             throw new MessagingException(0);
    486                         }
    487                         setupProtocolVersion(this, versions);
    488                     } catch (MessagingException e) {
    489                         bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE,
    490                                 MessagingException.PROTOCOL_VERSION_UNSUPPORTED);
    491                         return bundle;
    492                     }
    493 
    494                     // Run second test here for provisioning failures using FolderSync
    495                     userLog("Try folder sync");
    496                     // Send "0" as the sync key for new accounts; otherwise, use the current key
    497                     String syncKey = "0";
    498                     Account existingAccount = Utility.findExistingAccount(
    499                             context, -1L, hostAuth.mAddress, hostAuth.mLogin);
    500                     if (existingAccount != null && existingAccount.mSyncKey != null) {
    501                         syncKey = existingAccount.mSyncKey;
    502                     }
    503                     Serializer s = new Serializer();
    504                     s.start(Tags.FOLDER_FOLDER_SYNC).start(Tags.FOLDER_SYNC_KEY).text(syncKey)
    505                         .end().end().done();
    506                     resp = sendHttpClientPost("FolderSync", s.toByteArray());
    507                     code = resp.getStatus();
    508                     // Handle HTTP error responses accordingly
    509                     if (code == HttpStatus.SC_FORBIDDEN) {
    510                         // For validation only, we take 403 as ACCESS_DENIED (the account isn't
    511                         // authorized, possibly due to device type)
    512                         resultCode = MessagingException.ACCESS_DENIED;
    513                     } else if (resp.isProvisionError()) {
    514                         // The device needs to have security policies enforced
    515                         throw new CommandStatusException(CommandStatus.NEEDS_PROVISIONING);
    516                     } else if (code == HttpStatus.SC_NOT_FOUND) {
    517                         // We get a 404 from OWA addresses (which are NOT EAS addresses)
    518                         resultCode = MessagingException.PROTOCOL_VERSION_UNSUPPORTED;
    519                     } else if (code == HttpStatus.SC_UNAUTHORIZED) {
    520                         resultCode = resp.isMissingCertificate()
    521                                 ? MessagingException.CLIENT_CERTIFICATE_REQUIRED
    522                                 : MessagingException.AUTHENTICATION_FAILED;
    523                     } else if (code != HttpStatus.SC_OK) {
    524                         if ((code == EAS_REDIRECT_CODE) && (mRedirectCount++ < MAX_REDIRECTS) &&
    525                                 getValidateRedirect(resp, hostAuth)) {
    526                             return validateAccount(hostAuth, context);
    527                         }
    528                         // Fail generically with anything other than success
    529                         userLog("Unexpected response for FolderSync: ", code);
    530                         resultCode = MessagingException.UNSPECIFIED_EXCEPTION;
    531                     } else {
    532                         // We need to parse the result to see if we've got a provisioning issue
    533                         // (EAS 14.0 only)
    534                         if (!resp.isEmpty()) {
    535                             InputStream is = resp.getInputStream();
    536                             // Create the parser with statusOnly set to true; we only care about
    537                             // seeing if a CommandStatusException is thrown (indicating a
    538                             // provisioning failure)
    539                             new FolderSyncParser(is, new AccountSyncAdapter(this), true).parse();
    540                         }
    541                         userLog("Validation successful");
    542                     }
    543                 } else if (resp.isAuthError()) {
    544                     userLog("Authentication failed");
    545                     resultCode = resp.isMissingCertificate()
    546                             ? MessagingException.CLIENT_CERTIFICATE_REQUIRED
    547                             : MessagingException.AUTHENTICATION_FAILED;
    548                 } else if (code == INTERNAL_SERVER_ERROR_CODE) {
    549                     // For Exchange 2003, this could mean an authentication failure OR server error
    550                     userLog("Internal server error");
    551                     resultCode = MessagingException.AUTHENTICATION_FAILED_OR_SERVER_ERROR;
    552                 } else {
    553                     if ((code == EAS_REDIRECT_CODE) && (mRedirectCount++ < MAX_REDIRECTS) &&
    554                             getValidateRedirect(resp, hostAuth)) {
    555                         return validateAccount(hostAuth, context);
    556                     }
    557                     // TODO Need to catch other kinds of errors (e.g. policy) For now, report code.
    558                     userLog("Validation failed, reporting I/O error: ", code);
    559                     resultCode = MessagingException.IOERROR;
    560                 }
    561             } catch (CommandStatusException e) {
    562                 int status = e.mStatus;
    563                 if (CommandStatus.isNeedsProvisioning(status)) {
    564                     // Get the policies and see if we are able to support them
    565                     ProvisionParser pp = canProvision(this);
    566                     if (pp != null && pp.hasSupportablePolicySet()) {
    567                         // Set the proper result code and save the PolicySet in our Bundle
    568                         resultCode = MessagingException.SECURITY_POLICIES_REQUIRED;
    569                         bundle.putParcelable(EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET,
    570                                 pp.getPolicy());
    571                         if (mProtocolVersionDouble == Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
    572                             mAccount.mSecuritySyncKey = pp.getSecuritySyncKey();
    573                             if (!sendSettings()) {
    574                                 userLog("Denied access: ", CommandStatus.toString(status));
    575                                 resultCode = MessagingException.ACCESS_DENIED;
    576                             }
    577                         }
    578                     } else {
    579                         // If not, set the proper code (the account will not be created)
    580                         resultCode = MessagingException.SECURITY_POLICIES_UNSUPPORTED;
    581                         bundle.putParcelable(EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET,
    582                                 pp.getPolicy());
    583                     }
    584                 } else if (CommandStatus.isDeniedAccess(status)) {
    585                     userLog("Denied access: ", CommandStatus.toString(status));
    586                     resultCode = MessagingException.ACCESS_DENIED;
    587                 } else if (CommandStatus.isTransientError(status)) {
    588                     userLog("Transient error: ", CommandStatus.toString(status));
    589                     resultCode = MessagingException.IOERROR;
    590                 } else {
    591                     userLog("Unexpected response: ", CommandStatus.toString(status));
    592                     resultCode = MessagingException.UNSPECIFIED_EXCEPTION;
    593                 }
    594             } finally {
    595                 resp.close();
    596            }
    597         } catch (IOException e) {
    598             Throwable cause = e.getCause();
    599             if (cause != null && cause instanceof CertificateException) {
    600                 // This could be because the server's certificate failed to validate.
    601                 userLog("CertificateException caught: ", e.getMessage());
    602                 resultCode = MessagingException.GENERAL_SECURITY;
    603             }
    604             userLog("IOException caught: ", e.getMessage());
    605             resultCode = MessagingException.IOERROR;
    606         } catch (CertificateException e) {
    607             // This occurs if the client certificate the user specified is invalid/inaccessible.
    608             userLog("CertificateException caught: ", e.getMessage());
    609             resultCode = MessagingException.CLIENT_CERTIFICATE_ERROR;
    610         }
    611         bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, resultCode);
    612         return bundle;
    613     }
    614 
    615     /**
    616      * Gets the redirect location from the HTTP headers and uses that to modify the HttpPost so that
    617      * it can be reused
    618      *
    619      * @param resp the HttpResponse that indicates a redirect (451)
    620      * @param post the HttpPost that was originally sent to the server
    621      * @return the HttpPost, updated with the redirect location
    622      */
    623     private static HttpPost getRedirect(HttpResponse resp, HttpPost post) {
    624         Header locHeader = resp.getFirstHeader("X-MS-Location");
    625         if (locHeader != null) {
    626             String loc = locHeader.getValue();
    627             // If we've gotten one and it shows signs of looking like an address, we try
    628             // sending our request there
    629             if (loc != null && loc.startsWith("http")) {
    630                 post.setURI(URI.create(loc));
    631                 return post;
    632             }
    633         }
    634         return null;
    635     }
    636 
    637     /**
    638      * Send the POST command to the autodiscover server, handling a redirect, if necessary, and
    639      * return the HttpResponse.  If we get a 401 (unauthorized) error and we're using the
    640      * full email address, try the bare user name instead (e.g. foo instead of foo (at) bar.com)
    641      *
    642      * @param client the HttpClient to be used for the request
    643      * @param post the HttpPost we're going to send
    644      * @param canRetry whether we can retry using the bare name on an authentication failure (401)
    645      * @return an HttpResponse from the original or redirect server
    646      * @throws IOException on any IOException within the HttpClient code
    647      * @throws MessagingException
    648      */
    649     private EasResponse postAutodiscover(HttpClient client, HttpPost post, boolean canRetry)
    650             throws IOException, MessagingException {
    651         userLog("Posting autodiscover to: " + post.getURI());
    652         EasResponse resp = executePostWithTimeout(client, post, COMMAND_TIMEOUT);
    653         int code = resp.getStatus();
    654         // On a redirect, try the new location
    655         if (code == EAS_REDIRECT_CODE) {
    656             //post = getRedirect(resp.mResponse, post);
    657             if (post != null) {
    658                 userLog("Posting autodiscover to redirect: " + post.getURI());
    659                 return executePostWithTimeout(client, post, COMMAND_TIMEOUT);
    660             }
    661         // 401 (Unauthorized) is for true auth errors when used in Autodiscover
    662         } else if (code == HttpStatus.SC_UNAUTHORIZED) {
    663             if (canRetry && mUserName.contains("@")) {
    664                 // Try again using the bare user name
    665                 int atSignIndex = mUserName.indexOf('@');
    666                 mUserName = mUserName.substring(0, atSignIndex);
    667                 cacheAuthUserAndBaseUriStrings();
    668                 userLog("401 received; trying username: ", mUserName);
    669                 // Recreate the basic authentication string and reset the header
    670                 post.removeHeaders("Authorization");
    671                 post.setHeader("Authorization", mAuthString);
    672                 return postAutodiscover(client, post, false);
    673             }
    674             throw new MessagingException(MessagingException.AUTHENTICATION_FAILED);
    675         // 403 (and others) we'll just punt on
    676         } else if (code != HttpStatus.SC_OK) {
    677             // We'll try the next address if this doesn't work
    678             userLog("Code: " + code + ", throwing IOException");
    679             throw new IOException();
    680         }
    681         return resp;
    682     }
    683 
    684     /**
    685      * Convert an EAS server url to a HostAuth host address
    686      * @param url a url, as provided by the Exchange server
    687      * @return our equivalent host address
    688      */
    689     protected String autodiscoverUrlToHostAddress(String url) {
    690         if (url == null) return null;
    691         // We need to extract the server address from a url
    692         return Uri.parse(url).getHost();
    693     }
    694 
    695     /**
    696      * Use the Exchange 2007 AutoDiscover feature to try to retrieve server information using
    697      * only an email address and the password
    698      *
    699      * @param context Our {@link Context}.
    700      * @return a HostAuth ready to be saved in an Account or null (failure)
    701      */
    702     public Bundle tryAutodiscover(Context context, HostAuth hostAuth) throws RemoteException {
    703         XmlSerializer s = Xml.newSerializer();
    704         ByteArrayOutputStream os = new ByteArrayOutputStream(1024);
    705         Bundle bundle = new Bundle();
    706         bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
    707                 MessagingException.NO_ERROR);
    708         try {
    709             // Build the XML document that's sent to the autodiscover server(s)
    710             s.setOutput(os, "UTF-8");
    711             s.startDocument("UTF-8", false);
    712             s.startTag(null, "Autodiscover");
    713             s.attribute(null, "xmlns", AUTO_DISCOVER_SCHEMA_PREFIX + "requestschema/2006");
    714             s.startTag(null, "Request");
    715             s.startTag(null, "EMailAddress").text(hostAuth.mLogin).endTag(null, "EMailAddress");
    716             s.startTag(null, "AcceptableResponseSchema");
    717             s.text(AUTO_DISCOVER_SCHEMA_PREFIX + "responseschema/2006");
    718             s.endTag(null, "AcceptableResponseSchema");
    719             s.endTag(null, "Request");
    720             s.endTag(null, "Autodiscover");
    721             s.endDocument();
    722             String req = os.toString();
    723 
    724             // Initialize user name, password, etc.
    725             mContext = context;
    726             mHostAuth = hostAuth;
    727             mUserName = hostAuth.mLogin;
    728             mPassword = hostAuth.mPassword;
    729             mSsl = hostAuth.shouldUseSsl();
    730 
    731             // Make sure the authentication string is recreated and cached
    732             cacheAuthUserAndBaseUriStrings();
    733 
    734             // Split out the domain name
    735             int amp = mUserName.indexOf('@');
    736             // The UI ensures that userName is a valid email address
    737             if (amp < 0) {
    738                 throw new RemoteException();
    739             }
    740             String domain = mUserName.substring(amp + 1);
    741 
    742             // There are up to four attempts here; the two URLs that we're supposed to try per the
    743             // specification, and up to one redirect for each (handled in postAutodiscover)
    744             // Note: The expectation is that, of these four attempts, only a single server will
    745             // actually be identified as the autodiscover server.  For the identified server,
    746             // we may also try a 2nd connection with a different format (bare name).
    747 
    748             // Try the domain first and see if we can get a response
    749             HttpPost post = new HttpPost("https://" + domain + AUTO_DISCOVER_PAGE);
    750             setHeaders(post, false);
    751             post.setHeader("Content-Type", "text/xml");
    752             post.setEntity(new StringEntity(req));
    753             HttpClient client = getHttpClient(COMMAND_TIMEOUT);
    754             EasResponse resp;
    755             try {
    756                 resp = postAutodiscover(client, post, true /*canRetry*/);
    757             } catch (IOException e1) {
    758                 userLog("IOException in autodiscover; trying alternate address");
    759                 // We catch the IOException here because we have an alternate address to try
    760                 post.setURI(URI.create("https://autodiscover." + domain + AUTO_DISCOVER_PAGE));
    761                 // If we fail here, we're out of options, so we let the outer try catch the
    762                 // IOException and return null
    763                 resp = postAutodiscover(client, post, true /*canRetry*/);
    764             }
    765 
    766             try {
    767                 // Get the "final" code; if it's not 200, just return null
    768                 int code = resp.getStatus();
    769                 userLog("Code: " + code);
    770                 if (code != HttpStatus.SC_OK) return null;
    771 
    772                 InputStream is = resp.getInputStream();
    773                 // The response to Autodiscover is regular XML (not WBXML)
    774                 // If we ever get an error in this process, we'll just punt and return null
    775                 XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
    776                 XmlPullParser parser = factory.newPullParser();
    777                 parser.setInput(is, "UTF-8");
    778                 int type = parser.getEventType();
    779                 if (type == XmlPullParser.START_DOCUMENT) {
    780                     type = parser.next();
    781                     if (type == XmlPullParser.START_TAG) {
    782                         String name = parser.getName();
    783                         if (name.equals("Autodiscover")) {
    784                             hostAuth = new HostAuth();
    785                             parseAutodiscover(parser, hostAuth);
    786                             // On success, we'll have a server address and login
    787                             if (hostAuth.mAddress != null) {
    788                                 // Fill in the rest of the HostAuth
    789                                 // We use the user name and password that were successful during
    790                                 // the autodiscover process
    791                                 hostAuth.mLogin = mUserName;
    792                                 hostAuth.mPassword = mPassword;
    793                                 // Note: there is no way we can auto-discover the proper client
    794                                 // SSL certificate to use, if one is needed.
    795                                 hostAuth.mPort = 443;
    796                                 hostAuth.mProtocol = Eas.PROTOCOL;
    797                                 hostAuth.mFlags =
    798                                     HostAuth.FLAG_SSL | HostAuth.FLAG_AUTHENTICATE;
    799                                 bundle.putParcelable(
    800                                         EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH, hostAuth);
    801                             } else {
    802                                 bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
    803                                         MessagingException.UNSPECIFIED_EXCEPTION);
    804                             }
    805                         }
    806                     }
    807                 }
    808             } catch (XmlPullParserException e1) {
    809                 // This would indicate an I/O error of some sort
    810                 // We will simply return null and user can configure manually
    811             } finally {
    812                resp.close();
    813             }
    814         // There's no reason at all for exceptions to be thrown, and it's ok if so.
    815         // We just won't do auto-discover; user can configure manually
    816        } catch (IllegalArgumentException e) {
    817              bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
    818                      MessagingException.UNSPECIFIED_EXCEPTION);
    819        } catch (IllegalStateException e) {
    820             bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
    821                     MessagingException.UNSPECIFIED_EXCEPTION);
    822        } catch (IOException e) {
    823             userLog("IOException in Autodiscover", e);
    824             bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
    825                     MessagingException.IOERROR);
    826         } catch (MessagingException e) {
    827             bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
    828                     MessagingException.AUTODISCOVER_AUTHENTICATION_FAILED);
    829         }
    830         return bundle;
    831     }
    832 
    833     void parseServer(XmlPullParser parser, HostAuth hostAuth)
    834             throws XmlPullParserException, IOException {
    835         boolean mobileSync = false;
    836         while (true) {
    837             int type = parser.next();
    838             if (type == XmlPullParser.END_TAG && parser.getName().equals("Server")) {
    839                 break;
    840             } else if (type == XmlPullParser.START_TAG) {
    841                 String name = parser.getName();
    842                 if (name.equals("Type")) {
    843                     if (parser.nextText().equals("MobileSync")) {
    844                         mobileSync = true;
    845                     }
    846                 } else if (mobileSync && name.equals("Url")) {
    847                     String hostAddress =
    848                         autodiscoverUrlToHostAddress(parser.nextText());
    849                     if (hostAddress != null) {
    850                         hostAuth.mAddress = hostAddress;
    851                         userLog("Autodiscover, server: " + hostAddress);
    852                     }
    853                 }
    854             }
    855         }
    856     }
    857 
    858     void parseSettings(XmlPullParser parser, HostAuth hostAuth)
    859             throws XmlPullParserException, IOException {
    860         while (true) {
    861             int type = parser.next();
    862             if (type == XmlPullParser.END_TAG && parser.getName().equals("Settings")) {
    863                 break;
    864             } else if (type == XmlPullParser.START_TAG) {
    865                 String name = parser.getName();
    866                 if (name.equals("Server")) {
    867                     parseServer(parser, hostAuth);
    868                 }
    869             }
    870         }
    871     }
    872 
    873     void parseAction(XmlPullParser parser, HostAuth hostAuth)
    874             throws XmlPullParserException, IOException {
    875         while (true) {
    876             int type = parser.next();
    877             if (type == XmlPullParser.END_TAG && parser.getName().equals("Action")) {
    878                 break;
    879             } else if (type == XmlPullParser.START_TAG) {
    880                 String name = parser.getName();
    881                 if (name.equals("Error")) {
    882                     // Should parse the error
    883                 } else if (name.equals("Redirect")) {
    884                     LogUtils.d(TAG, "Redirect: " + parser.nextText());
    885                 } else if (name.equals("Settings")) {
    886                     parseSettings(parser, hostAuth);
    887                 }
    888             }
    889         }
    890     }
    891 
    892     void parseUser(XmlPullParser parser, HostAuth hostAuth)
    893             throws XmlPullParserException, IOException {
    894         while (true) {
    895             int type = parser.next();
    896             if (type == XmlPullParser.END_TAG && parser.getName().equals("User")) {
    897                 break;
    898             } else if (type == XmlPullParser.START_TAG) {
    899                 String name = parser.getName();
    900                 if (name.equals("EMailAddress")) {
    901                     String addr = parser.nextText();
    902                     userLog("Autodiscover, email: " + addr);
    903                 } else if (name.equals("DisplayName")) {
    904                     String dn = parser.nextText();
    905                     userLog("Autodiscover, user: " + dn);
    906                 }
    907             }
    908         }
    909     }
    910 
    911     void parseResponse(XmlPullParser parser, HostAuth hostAuth)
    912             throws XmlPullParserException, IOException {
    913         while (true) {
    914             int type = parser.next();
    915             if (type == XmlPullParser.END_TAG && parser.getName().equals("Response")) {
    916                 break;
    917             } else if (type == XmlPullParser.START_TAG) {
    918                 String name = parser.getName();
    919                 if (name.equals("User")) {
    920                     parseUser(parser, hostAuth);
    921                 } else if (name.equals("Action")) {
    922                     parseAction(parser, hostAuth);
    923                 }
    924             }
    925         }
    926     }
    927 
    928     void parseAutodiscover(XmlPullParser parser, HostAuth hostAuth)
    929             throws XmlPullParserException, IOException {
    930         while (true) {
    931             int type = parser.nextTag();
    932             if (type == XmlPullParser.END_TAG && parser.getName().equals("Autodiscover")) {
    933                 break;
    934             } else if (type == XmlPullParser.START_TAG && parser.getName().equals("Response")) {
    935                 parseResponse(parser, hostAuth);
    936             }
    937         }
    938     }
    939 
    940     /**
    941      * Contact the GAL and obtain a list of matching accounts
    942      * @param context caller's context
    943      * @param accountId the account Id to search
    944      * @param filter the characters entered so far
    945      * @return a result record or null for no data
    946      *
    947      * TODO: shorter timeout for interactive lookup
    948      * TODO: make watchdog actually work (it doesn't understand our service w/Mailbox == 0)
    949      * TODO: figure out why sendHttpClientPost() hangs - possibly pool exhaustion
    950      */
    951     static public GalResult searchGal(Context context, long accountId, String filter, int limit) {
    952         Account acct = Account.restoreAccountWithId(context, accountId);
    953         if (acct != null) {
    954             EasSyncService svc = setupServiceForAccount(context, acct);
    955             if (svc == null) return null;
    956             try {
    957                 Serializer s = new Serializer();
    958                 s.start(Tags.SEARCH_SEARCH).start(Tags.SEARCH_STORE);
    959                 s.data(Tags.SEARCH_NAME, "GAL").data(Tags.SEARCH_QUERY, filter);
    960                 s.start(Tags.SEARCH_OPTIONS);
    961                 s.data(Tags.SEARCH_RANGE, "0-" + Integer.toString(limit - 1));
    962                 s.end().end().end().done();
    963                 EasResponse resp = svc.sendHttpClientPost("Search", s.toByteArray());
    964                 try {
    965                     int code = resp.getStatus();
    966                     if (code == HttpStatus.SC_OK) {
    967                         InputStream is = resp.getInputStream();
    968                         try {
    969                             GalParser gp = new GalParser(is, svc);
    970                             if (gp.parse()) {
    971                                 return gp.getGalResult();
    972                             }
    973                         } finally {
    974                             is.close();
    975                         }
    976                     } else {
    977                         svc.userLog("GAL lookup returned " + code);
    978                     }
    979                 } finally {
    980                     resp.close();
    981                 }
    982             } catch (IOException e) {
    983                 // GAL is non-critical; we'll just go on
    984                 svc.userLog("GAL lookup exception " + e);
    985             }
    986         }
    987         return null;
    988     }
    989     /**
    990      * Send an email responding to a Message that has been marked as a meeting request.  The message
    991      * will consist a little bit of event information and an iCalendar attachment
    992      * @param msg the meeting request email
    993      */
    994     private void sendMeetingResponseMail(Message msg, int response) {
    995         // Get the meeting information; we'd better have some...
    996         if (msg.mMeetingInfo == null) return;
    997         PackedString meetingInfo = new PackedString(msg.mMeetingInfo);
    998 
    999         // This will come as "First Last" <box (at) server.blah>, so we use Address to
   1000         // parse it into parts; we only need the email address part for the ics file
   1001         Address[] addrs = Address.parse(meetingInfo.get(MeetingInfo.MEETING_ORGANIZER_EMAIL));
   1002         // It shouldn't be possible, but handle it anyway
   1003         if (addrs.length != 1) return;
   1004         String organizerEmail = addrs[0].getAddress();
   1005 
   1006         String dtStamp = meetingInfo.get(MeetingInfo.MEETING_DTSTAMP);
   1007         String dtStart = meetingInfo.get(MeetingInfo.MEETING_DTSTART);
   1008         String dtEnd = meetingInfo.get(MeetingInfo.MEETING_DTEND);
   1009 
   1010         // What we're doing here is to create an Entity that looks like an Event as it would be
   1011         // stored by CalendarProvider
   1012         ContentValues entityValues = new ContentValues();
   1013         Entity entity = new Entity(entityValues);
   1014 
   1015         // Fill in times, location, title, and organizer
   1016         entityValues.put("DTSTAMP",
   1017                 CalendarUtilities.convertEmailDateTimeToCalendarDateTime(dtStamp));
   1018         entityValues.put(Events.DTSTART, Utility.parseEmailDateTimeToMillis(dtStart));
   1019         entityValues.put(Events.DTEND, Utility.parseEmailDateTimeToMillis(dtEnd));
   1020         entityValues.put(Events.EVENT_LOCATION, meetingInfo.get(MeetingInfo.MEETING_LOCATION));
   1021         entityValues.put(Events.TITLE, meetingInfo.get(MeetingInfo.MEETING_TITLE));
   1022         entityValues.put(Events.ORGANIZER, organizerEmail);
   1023 
   1024         // Add ourselves as an attendee, using our account email address
   1025         ContentValues attendeeValues = new ContentValues();
   1026         attendeeValues.put(Attendees.ATTENDEE_RELATIONSHIP,
   1027                 Attendees.RELATIONSHIP_ATTENDEE);
   1028         attendeeValues.put(Attendees.ATTENDEE_EMAIL, mAccount.mEmailAddress);
   1029         entity.addSubValue(Attendees.CONTENT_URI, attendeeValues);
   1030 
   1031         // Add the organizer
   1032         ContentValues organizerValues = new ContentValues();
   1033         organizerValues.put(Attendees.ATTENDEE_RELATIONSHIP,
   1034                 Attendees.RELATIONSHIP_ORGANIZER);
   1035         organizerValues.put(Attendees.ATTENDEE_EMAIL, organizerEmail);
   1036         entity.addSubValue(Attendees.CONTENT_URI, organizerValues);
   1037 
   1038         // Create a message from the Entity we've built.  The message will have fields like
   1039         // to, subject, date, and text filled in.  There will also be an "inline" attachment
   1040         // which is in iCalendar format
   1041         int flag;
   1042         switch(response) {
   1043             case EmailServiceConstants.MEETING_REQUEST_ACCEPTED:
   1044                 flag = Message.FLAG_OUTGOING_MEETING_ACCEPT;
   1045                 break;
   1046             case EmailServiceConstants.MEETING_REQUEST_DECLINED:
   1047                 flag = Message.FLAG_OUTGOING_MEETING_DECLINE;
   1048                 break;
   1049             case EmailServiceConstants.MEETING_REQUEST_TENTATIVE:
   1050             default:
   1051                 flag = Message.FLAG_OUTGOING_MEETING_TENTATIVE;
   1052                 break;
   1053         }
   1054         Message outgoingMsg =
   1055             CalendarUtilities.createMessageForEntity(mContext, entity, flag,
   1056                     meetingInfo.get(MeetingInfo.MEETING_UID), mAccount);
   1057         // Assuming we got a message back (we might not if the event has been deleted), send it
   1058         if (outgoingMsg != null) {
   1059             EasOutboxService.sendMessage(mContext, mAccount.mId, outgoingMsg);
   1060         }
   1061     }
   1062 
   1063     /**
   1064      * Responds to a move request.  The MessageMoveRequest is basically our
   1065      * wrapper for the MoveItems service call
   1066      * @param req the request (message id and "to" mailbox id)
   1067      * @throws IOException
   1068      */
   1069     protected void messageMoveRequest(MessageMoveRequest req) throws IOException {
   1070         // Retrieve the message and mailbox; punt if either are null
   1071         Message msg = Message.restoreMessageWithId(mContext, req.mMessageId);
   1072         if (msg == null) return;
   1073         Cursor c = mContentResolver.query(ContentUris.withAppendedId(Message.UPDATED_CONTENT_URI,
   1074                 msg.mId), new String[] {MessageColumns.MAILBOX_KEY}, null, null, null);
   1075         if (c == null) throw new ProviderUnavailableException();
   1076         Mailbox srcMailbox = null;
   1077         try {
   1078             if (!c.moveToNext()) return;
   1079             srcMailbox = Mailbox.restoreMailboxWithId(mContext, c.getLong(0));
   1080         } finally {
   1081             c.close();
   1082         }
   1083         if (srcMailbox == null) return;
   1084         Mailbox dstMailbox = Mailbox.restoreMailboxWithId(mContext, req.mMailboxId);
   1085         if (dstMailbox == null) return;
   1086         Serializer s = new Serializer();
   1087         s.start(Tags.MOVE_MOVE_ITEMS).start(Tags.MOVE_MOVE);
   1088         s.data(Tags.MOVE_SRCMSGID, msg.mServerId);
   1089         s.data(Tags.MOVE_SRCFLDID, srcMailbox.mServerId);
   1090         s.data(Tags.MOVE_DSTFLDID, dstMailbox.mServerId);
   1091         s.end().end().done();
   1092         EasResponse resp = sendHttpClientPost("MoveItems", s.toByteArray());
   1093         try {
   1094             int status = resp.getStatus();
   1095             if (status == HttpStatus.SC_OK) {
   1096                 if (!resp.isEmpty()) {
   1097                     InputStream is = resp.getInputStream();
   1098                     MoveItemsParser p = new MoveItemsParser(is);
   1099                     p.parse();
   1100                     int statusCode = p.getStatusCode();
   1101                     ContentValues cv = new ContentValues();
   1102                     if (statusCode == MoveItemsParser.STATUS_CODE_REVERT) {
   1103                         // Restore the old mailbox id
   1104                         cv.put(MessageColumns.MAILBOX_KEY, srcMailbox.mServerId);
   1105                         mContentResolver.update(
   1106                                 ContentUris.withAppendedId(Message.CONTENT_URI, req.mMessageId),
   1107                                 cv, null, null);
   1108                     } else if (statusCode == MoveItemsParser.STATUS_CODE_SUCCESS) {
   1109                         // Update with the new server id
   1110                         cv.put(SyncColumns.SERVER_ID, p.getNewServerId());
   1111                         cv.put(Message.FLAGS, msg.mFlags | MESSAGE_FLAG_MOVED_MESSAGE);
   1112                         mContentResolver.update(
   1113                                 ContentUris.withAppendedId(Message.CONTENT_URI, req.mMessageId),
   1114                                 cv, null, null);
   1115                     }
   1116                     if (statusCode == MoveItemsParser.STATUS_CODE_SUCCESS
   1117                             || statusCode == MoveItemsParser.STATUS_CODE_REVERT) {
   1118                         // If we revert or succeed, we no longer need the update information
   1119                         // OR the now-duplicate email (the new copy will be synced down)
   1120                         mContentResolver.delete(ContentUris.withAppendedId(
   1121                                 Message.UPDATED_CONTENT_URI, req.mMessageId), null, null);
   1122                     } else {
   1123                         // In this case, we're retrying, so do nothing.  The request will be
   1124                         // handled next sync
   1125                     }
   1126                 }
   1127             } else if (resp.isAuthError()) {
   1128                 throw new EasAuthenticationException();
   1129             } else {
   1130                 userLog("Move items request failed, code: " + status);
   1131                 throw new IOException();
   1132             }
   1133         } finally {
   1134             resp.close();
   1135         }
   1136     }
   1137 
   1138     /**
   1139      * Responds to a meeting request.  The MeetingResponseRequest is basically our
   1140      * wrapper for the meetingResponse service call
   1141      * @param req the request (message id and response code)
   1142      * @throws IOException
   1143      */
   1144     protected void sendMeetingResponse(MeetingResponseRequest req) throws IOException {
   1145         // Retrieve the message and mailbox; punt if either are null
   1146         Message msg = Message.restoreMessageWithId(mContext, req.mMessageId);
   1147         if (msg == null) return;
   1148         Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, msg.mMailboxKey);
   1149         if (mailbox == null) return;
   1150         Serializer s = new Serializer();
   1151         s.start(Tags.MREQ_MEETING_RESPONSE).start(Tags.MREQ_REQUEST);
   1152         s.data(Tags.MREQ_USER_RESPONSE, Integer.toString(req.mResponse));
   1153         s.data(Tags.MREQ_COLLECTION_ID, mailbox.mServerId);
   1154         s.data(Tags.MREQ_REQ_ID, msg.mServerId);
   1155         s.end().end().done();
   1156         EasResponse resp = sendHttpClientPost("MeetingResponse", s.toByteArray());
   1157         try {
   1158             int status = resp.getStatus();
   1159             if (status == HttpStatus.SC_OK) {
   1160                 if (!resp.isEmpty()) {
   1161                     InputStream is = resp.getInputStream();
   1162                     new MeetingResponseParser(is).parse();
   1163                     String meetingInfo = msg.mMeetingInfo;
   1164                     if (meetingInfo != null) {
   1165                         String responseRequested = new PackedString(meetingInfo).get(
   1166                                 MeetingInfo.MEETING_RESPONSE_REQUESTED);
   1167                         // If there's no tag, or a non-zero tag, we send the response mail
   1168                         if ("0".equals(responseRequested)) {
   1169                             return;
   1170                         }
   1171                     }
   1172                     sendMeetingResponseMail(msg, req.mResponse);
   1173                 }
   1174             } else if (resp.isAuthError()) {
   1175                 throw new EasAuthenticationException();
   1176             } else {
   1177                 userLog("Meeting response request failed, code: " + status);
   1178                 throw new IOException();
   1179             }
   1180         } finally {
   1181             resp.close();
   1182        }
   1183     }
   1184 
   1185     /**
   1186      * Using mUserName and mPassword, lazily create the strings that are commonly used in our HTTP
   1187      * POSTs, including the authentication header string, the base URI we use to communicate with
   1188      * EAS, and the user information string (user, deviceId, and deviceType)
   1189      */
   1190     private void cacheAuthUserAndBaseUriStrings() {
   1191         if (mAuthString == null || mUserString == null || mBaseUriString == null) {
   1192             String safeUserName = Uri.encode(mUserName);
   1193             String cs = mUserName + ':' + mPassword;
   1194             mAuthString = "Basic " + Base64.encodeToString(cs.getBytes(), Base64.NO_WRAP);
   1195             mUserString = "&User=" + safeUserName + "&DeviceId=" + mDeviceId +
   1196                 "&DeviceType=" + DEVICE_TYPE;
   1197             String scheme =
   1198                 EmailClientConnectionManager.makeScheme(mSsl, mTrustSsl, mClientCertAlias);
   1199             mBaseUriString = scheme + "://" + mHostAddress + "/Microsoft-Server-ActiveSync";
   1200         }
   1201     }
   1202 
   1203     @VisibleForTesting
   1204     String makeUriString(String cmd, String extra) {
   1205         cacheAuthUserAndBaseUriStrings();
   1206         String uriString = mBaseUriString;
   1207         if (cmd != null) {
   1208             uriString += "?Cmd=" + cmd + mUserString;
   1209         }
   1210         if (extra != null) {
   1211             uriString += extra;
   1212         }
   1213         return uriString;
   1214     }
   1215 
   1216     /**
   1217      * Set standard HTTP headers, using a policy key if required
   1218      * @param method the method we are going to send
   1219      * @param usePolicyKey whether or not a policy key should be sent in the headers
   1220      */
   1221     /*package*/ void setHeaders(HttpRequestBase method, boolean usePolicyKey) {
   1222         method.setHeader("Authorization", mAuthString);
   1223         method.setHeader("MS-ASProtocolVersion", mProtocolVersion);
   1224         method.setHeader("User-Agent", USER_AGENT);
   1225         method.setHeader("Accept-Encoding", "gzip");
   1226         if (usePolicyKey) {
   1227             // If there's an account in existence, use its key; otherwise (we're creating the
   1228             // account), send "0".  The server will respond with code 449 if there are policies
   1229             // to be enforced
   1230             String key = "0";
   1231             if (mAccount != null) {
   1232                 String accountKey = mAccount.mSecuritySyncKey;
   1233                 if (!TextUtils.isEmpty(accountKey)) {
   1234                     key = accountKey;
   1235                 }
   1236             }
   1237             method.setHeader("X-MS-PolicyKey", key);
   1238         }
   1239     }
   1240 
   1241     protected void setConnectionParameters(HostAuth hostAuth) throws CertificateException {
   1242         mHostAuth = hostAuth;
   1243         mSsl = hostAuth.shouldUseSsl();
   1244         mTrustSsl = hostAuth.shouldTrustAllServerCerts();
   1245         mClientCertAlias = hostAuth.mClientCertAlias;
   1246 
   1247         // Register the new alias, if needed.
   1248         if (mClientCertAlias != null) {
   1249             // Ensure that the connection manager knows to use the proper client certificate
   1250             // when establishing connections for this service.
   1251             EmailClientConnectionManager connManager = getClientConnectionManager();
   1252             connManager.registerClientCert(mContext, hostAuth);
   1253         }
   1254     }
   1255 
   1256     private EmailClientConnectionManager getClientConnectionManager() {
   1257         return ExchangeService.getClientConnectionManager(mContext, mHostAuth);
   1258     }
   1259 
   1260     private HttpClient getHttpClient(int timeout) {
   1261         HttpParams params = new BasicHttpParams();
   1262         HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT);
   1263         HttpConnectionParams.setSoTimeout(params, timeout);
   1264         HttpConnectionParams.setSocketBufferSize(params, 8192);
   1265         return new DefaultHttpClient(getClientConnectionManager(), params) {
   1266             protected BasicHttpProcessor createHttpProcessor() {
   1267                 final BasicHttpProcessor processor = super.createHttpProcessor();
   1268                 processor.addRequestInterceptor(new CurlLogger());
   1269                 return processor;
   1270             }
   1271         };
   1272     }
   1273 
   1274     public EasResponse sendHttpClientPost(String cmd, byte[] bytes) throws IOException {
   1275         return sendHttpClientPost(cmd, new ByteArrayEntity(bytes), COMMAND_TIMEOUT);
   1276     }
   1277 
   1278     /**
   1279      * Convenience method for executePostWithTimeout for use other than with the Ping command
   1280      */
   1281     protected EasResponse executePostWithTimeout(HttpClient client, HttpPost method, int timeout)
   1282             throws IOException {
   1283         return executePostWithTimeout(client, method, timeout, false);
   1284     }
   1285 
   1286     /**
   1287      * Handle executing an HTTP POST command with proper timeout, watchdog, and ping behavior
   1288      * @param client the HttpClient
   1289      * @param method the HttpPost
   1290      * @param timeout the timeout before failure, in ms
   1291      * @param isPingCommand whether the POST is for the Ping command (requires wakelock logic)
   1292      * @return the HttpResponse
   1293      * @throws IOException
   1294      */
   1295     protected EasResponse executePostWithTimeout(HttpClient client, HttpPost method,
   1296             final int timeout, final boolean isPingCommand) throws IOException {
   1297         final boolean hasWakeLock;
   1298         synchronized(getSynchronizer()) {
   1299             hasWakeLock = ExchangeService.isHoldingWakeLock(mMailboxId);
   1300             if (isPingCommand && !hasWakeLock) {
   1301                 LogUtils.e(TAG, "executePostWithTimeout (ping) without holding wakelock");
   1302             }
   1303             mPendingPost = method;
   1304             long alarmTime = timeout + WATCHDOG_TIMEOUT_ALLOWANCE;
   1305             if (isPingCommand && hasWakeLock) {
   1306                 ExchangeService.runAsleep(mMailboxId, alarmTime);
   1307             } else {
   1308                 ExchangeService.setWatchdogAlarm(mMailboxId, alarmTime);
   1309             }
   1310         }
   1311         try {
   1312             return EasResponse.fromHttpRequest(getClientConnectionManager(), client, method);
   1313         } finally {
   1314             synchronized(getSynchronizer()) {
   1315                 if (isPingCommand && hasWakeLock) {
   1316                     ExchangeService.runAwake(mMailboxId);
   1317                 } else {
   1318                     ExchangeService.clearWatchdogAlarm(mMailboxId);
   1319                 }
   1320                 mPendingPost = null;
   1321             }
   1322         }
   1323     }
   1324 
   1325     public EasResponse sendHttpClientPost(String cmd, HttpEntity entity, int timeout)
   1326             throws IOException {
   1327         HttpClient client = getHttpClient(timeout);
   1328         boolean isPingCommand = cmd.equals(PING_COMMAND);
   1329 
   1330         // Split the mail sending commands
   1331         String extra = null;
   1332         boolean msg = false;
   1333         if (cmd.startsWith("SmartForward&") || cmd.startsWith("SmartReply&")) {
   1334             int cmdLength = cmd.indexOf('&');
   1335             extra = cmd.substring(cmdLength);
   1336             cmd = cmd.substring(0, cmdLength);
   1337             msg = true;
   1338         } else if (cmd.startsWith("SendMail&")) {
   1339             msg = true;
   1340         }
   1341 
   1342         String us = makeUriString(cmd, extra);
   1343         HttpPost method = new HttpPost(URI.create(us));
   1344         // Send the proper Content-Type header; it's always wbxml except for messages when
   1345         // the EAS protocol version is < 14.0
   1346         // If entity is null (e.g. for attachments), don't set this header
   1347         if (msg && (mProtocolVersionDouble < Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE)) {
   1348             method.setHeader("Content-Type", "message/rfc822");
   1349         } else if (entity != null) {
   1350             method.setHeader("Content-Type", "application/vnd.ms-sync.wbxml");
   1351         }
   1352         setHeaders(method, !isPingCommand);
   1353         // NOTE
   1354         // The next lines are added at the insistence of $VENDOR, who is seeing inappropriate
   1355         // network activity related to the Ping command on some networks with some servers.
   1356         // This code should be removed when the underlying issue is resolved
   1357         if (isPingCommand) {
   1358             method.setHeader("Connection", "close");
   1359         }
   1360         method.setEntity(entity);
   1361         return executePostWithTimeout(client, method, timeout, isPingCommand);
   1362     }
   1363 
   1364     protected EasResponse sendHttpClientOptions() throws IOException {
   1365         cacheAuthUserAndBaseUriStrings();
   1366         // For OPTIONS, just use the base string and the single header
   1367         String uriString = mBaseUriString;
   1368         HttpOptions method = new HttpOptions(URI.create(uriString));
   1369         method.setHeader("Authorization", mAuthString);
   1370         method.setHeader("User-Agent", USER_AGENT);
   1371         HttpClient client = getHttpClient(COMMAND_TIMEOUT);
   1372         return EasResponse.fromHttpRequest(getClientConnectionManager(), client, method);
   1373     }
   1374 
   1375     String getTargetCollectionClassFromCursor(Cursor c) {
   1376         int type = c.getInt(Mailbox.CONTENT_TYPE_COLUMN);
   1377         if (type == Mailbox.TYPE_CONTACTS) {
   1378             return "Contacts";
   1379         } else if (type == Mailbox.TYPE_CALENDAR) {
   1380             return "Calendar";
   1381         } else {
   1382             return "Email";
   1383         }
   1384     }
   1385 
   1386     /**
   1387      * Negotiate provisioning with the server.  First, get policies form the server and see if
   1388      * the policies are supported by the device.  Then, write the policies to the account and
   1389      * tell SecurityPolicy that we have policies in effect.  Finally, see if those policies are
   1390      * active; if so, acknowledge the policies to the server and get a final policy key that we
   1391      * use in future EAS commands and write this key to the account.
   1392      * @return whether or not provisioning has been successful
   1393      * @throws IOException
   1394      */
   1395     public static boolean tryProvision(EasSyncService svc) throws IOException {
   1396         // First, see if provisioning is even possible, i.e. do we support the policies required
   1397         // by the server
   1398         ProvisionParser pp = canProvision(svc);
   1399         if (pp == null) return false;
   1400         Context context = svc.mContext;
   1401         Account account = svc.mAccount;
   1402         // Get the policies from ProvisionParser
   1403         Policy policy = pp.getPolicy();
   1404         Policy oldPolicy = null;
   1405         // Grab the old policy (if any)
   1406         if (svc.mAccount.mPolicyKey > 0) {
   1407             oldPolicy = Policy.restorePolicyWithId(context, account.mPolicyKey);
   1408         }
   1409         // Update the account with a null policyKey (the key we've gotten is
   1410         // temporary and cannot be used for syncing)
   1411         PolicyServiceProxy.setAccountPolicy(context, account.mId, policy, null);
   1412         // Make sure mAccount is current (with latest policy key)
   1413         account.refresh(context);
   1414         if (pp.getRemoteWipe()) {
   1415             // We've gotten a remote wipe command
   1416             ExchangeService.alwaysLog("!!! Remote wipe request received");
   1417             // Start by setting the account to security hold
   1418             PolicyServiceProxy.setAccountHoldFlag(context, account, true);
   1419             // Force a stop to any running syncs for this account (except this one)
   1420             ExchangeService.stopNonAccountMailboxSyncsForAccount(account.mId);
   1421 
   1422             // First, we've got to acknowledge it, but wrap the wipe in try/catch so that
   1423             // we wipe the device regardless of any errors in acknowledgment
   1424             try {
   1425                 ExchangeService.alwaysLog("!!! Acknowledging remote wipe to server");
   1426                 acknowledgeRemoteWipe(svc, pp.getSecuritySyncKey());
   1427             } catch (Exception e) {
   1428                 // Because remote wipe is such a high priority task, we don't want to
   1429                 // circumvent it if there's an exception in acknowledgment
   1430             }
   1431             // Then, tell SecurityPolicy to wipe the device
   1432             ExchangeService.alwaysLog("!!! Executing remote wipe");
   1433             PolicyServiceProxy.remoteWipe(context);
   1434             return false;
   1435         } else if (pp.hasSupportablePolicySet() && PolicyServiceProxy.isActive(context, policy)) {
   1436             // See if the required policies are in force; if they are, acknowledge the policies
   1437             // to the server and get the final policy key
   1438             // NOTE: For EAS 14.0, we already have the acknowledgment in the ProvisionParser
   1439             String securitySyncKey;
   1440             if (svc.mProtocolVersionDouble == Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
   1441                 securitySyncKey = pp.getSecuritySyncKey();
   1442             } else {
   1443                 securitySyncKey = acknowledgeProvision(svc, pp.getSecuritySyncKey(),
   1444                         PROVISION_STATUS_OK);
   1445             }
   1446             if (securitySyncKey != null) {
   1447                 // If attachment policies have changed, fix up any affected attachment records
   1448                 if (oldPolicy != null) {
   1449                     if ((oldPolicy.mDontAllowAttachments != policy.mDontAllowAttachments) ||
   1450                             (oldPolicy.mMaxAttachmentSize != policy.mMaxAttachmentSize)) {
   1451                         Policy.setAttachmentFlagsForNewPolicy(context, account, policy);
   1452                     }
   1453                 }
   1454                 // Write the final policy key to the Account and say we've been successful
   1455                 PolicyServiceProxy.setAccountPolicy(context, account.mId, policy, securitySyncKey);
   1456                 // Release any mailboxes that might be in a security hold
   1457                 ExchangeService.releaseSecurityHold(account);
   1458                 return true;
   1459             }
   1460         }
   1461         return false;
   1462     }
   1463 
   1464     private static String getPolicyType(Double protocolVersion) {
   1465         return (protocolVersion >=
   1466             Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) ? EAS_12_POLICY_TYPE : EAS_2_POLICY_TYPE;
   1467     }
   1468 
   1469     /**
   1470      * Obtain a set of policies from the server and determine whether those policies are supported
   1471      * by the device.
   1472      * @return the ProvisionParser (holds policies and key) if we receive policies; null otherwise
   1473      * @throws IOException
   1474      */
   1475     public static ProvisionParser canProvision(EasSyncService svc) throws IOException {
   1476         Serializer s = new Serializer();
   1477         Double protocolVersion = svc.mProtocolVersionDouble;
   1478         s.start(Tags.PROVISION_PROVISION);
   1479         if (svc.mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2010_SP1_DOUBLE) {
   1480             // Send settings information in 14.1 and greater
   1481             s.start(Tags.SETTINGS_DEVICE_INFORMATION).start(Tags.SETTINGS_SET);
   1482             s.data(Tags.SETTINGS_MODEL, Build.MODEL);
   1483             //s.data(Tags.SETTINGS_IMEI, "");
   1484             //s.data(Tags.SETTINGS_FRIENDLY_NAME, "Friendly Name");
   1485             s.data(Tags.SETTINGS_OS, "Android " + Build.VERSION.RELEASE);
   1486             //s.data(Tags.SETTINGS_OS_LANGUAGE, "");
   1487             //s.data(Tags.SETTINGS_PHONE_NUMBER, "");
   1488             //s.data(Tags.SETTINGS_MOBILE_OPERATOR, "");
   1489             s.data(Tags.SETTINGS_USER_AGENT, EasSyncService.USER_AGENT);
   1490             s.end().end();  // SETTINGS_SET, SETTINGS_DEVICE_INFORMATION
   1491         }
   1492         s.start(Tags.PROVISION_POLICIES);
   1493         s.start(Tags.PROVISION_POLICY);
   1494         s.data(Tags.PROVISION_POLICY_TYPE, getPolicyType(protocolVersion));
   1495         s.end().end().end().done(); // PROVISION_POLICY, PROVISION_POLICIES, PROVISION_PROVISION
   1496         EasResponse resp = svc.sendHttpClientPost("Provision", s.toByteArray());
   1497         try {
   1498             int code = resp.getStatus();
   1499             if (code == HttpStatus.SC_OK) {
   1500                 InputStream is = resp.getInputStream();
   1501                 ProvisionParser pp = new ProvisionParser(svc.mContext, is);
   1502                 if (pp.parse()) {
   1503                     // The PolicySet in the ProvisionParser will have the requirements for all KNOWN
   1504                     // policies.  If others are required, hasSupportablePolicySet will be false
   1505                     if (pp.hasSupportablePolicySet() &&
   1506                             svc.mProtocolVersionDouble == Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE) {
   1507                         // In EAS 14.0, we need the final security key in order to use the settings
   1508                         // command
   1509                         String policyKey = acknowledgeProvision(svc, pp.getSecuritySyncKey(),
   1510                                 PROVISION_STATUS_OK);
   1511                         if (policyKey != null) {
   1512                             pp.setSecuritySyncKey(policyKey);
   1513                         }
   1514                     } else if (!pp.hasSupportablePolicySet())  {
   1515                         // Try to acknowledge using the "partial" status (i.e. we can partially
   1516                         // accommodate the required policies).  The server will agree to this if the
   1517                         // "allow non-provisionable devices" setting is enabled on the server
   1518                         ExchangeService.log("PolicySet is NOT fully supportable");
   1519                         if (acknowledgeProvision(svc, pp.getSecuritySyncKey(),
   1520                                 PROVISION_STATUS_PARTIAL) != null) {
   1521                             // The server's ok with our inability to support policies, so we'll
   1522                             // clear them
   1523                             pp.clearUnsupportablePolicies();
   1524                         }
   1525                     }
   1526                     return pp;
   1527                 }
   1528             }
   1529         } finally {
   1530             resp.close();
   1531         }
   1532 
   1533         // On failures, simply return null
   1534         return null;
   1535     }
   1536 
   1537     /**
   1538      * Acknowledge that we support the policies provided by the server, and that these policies
   1539      * are in force.
   1540      * @param tempKey the initial (temporary) policy key sent by the server
   1541      * @throws IOException
   1542      */
   1543     private static void acknowledgeRemoteWipe(EasSyncService svc, String tempKey)
   1544             throws IOException {
   1545         acknowledgeProvisionImpl(svc, tempKey, PROVISION_STATUS_OK, true);
   1546     }
   1547 
   1548     private static String acknowledgeProvision(EasSyncService svc, String tempKey, String result)
   1549             throws IOException {
   1550         return acknowledgeProvisionImpl(svc, tempKey, result, false);
   1551     }
   1552 
   1553     private static String acknowledgeProvisionImpl(EasSyncService svc, String tempKey,
   1554             String status, boolean remoteWipe) throws IOException {
   1555         Serializer s = new Serializer();
   1556         s.start(Tags.PROVISION_PROVISION).start(Tags.PROVISION_POLICIES);
   1557         s.start(Tags.PROVISION_POLICY);
   1558 
   1559         // Use the proper policy type, depending on EAS version
   1560         s.data(Tags.PROVISION_POLICY_TYPE, getPolicyType(svc.mProtocolVersionDouble));
   1561 
   1562         s.data(Tags.PROVISION_POLICY_KEY, tempKey);
   1563         s.data(Tags.PROVISION_STATUS, status);
   1564         s.end().end(); // PROVISION_POLICY, PROVISION_POLICIES
   1565         if (remoteWipe) {
   1566             s.start(Tags.PROVISION_REMOTE_WIPE);
   1567             s.data(Tags.PROVISION_STATUS, PROVISION_STATUS_OK);
   1568             s.end();
   1569         }
   1570         s.end().done(); // PROVISION_PROVISION
   1571         EasResponse resp = svc.sendHttpClientPost("Provision", s.toByteArray());
   1572         try {
   1573             int code = resp.getStatus();
   1574             if (code == HttpStatus.SC_OK) {
   1575                 InputStream is = resp.getInputStream();
   1576                 ProvisionParser pp = new ProvisionParser(svc.mContext, is);
   1577                 if (pp.parse()) {
   1578                     // Return the final policy key from the ProvisionParser
   1579                     String result = (pp.getSecuritySyncKey() == null) ? "failed" : "confirmed";
   1580                     ExchangeService.log("Provision " + result + " for " +
   1581                             (PROVISION_STATUS_PARTIAL.equals(status) ? "PART" : "FULL") + " set");
   1582                     return pp.getSecuritySyncKey();
   1583                 }
   1584             }
   1585         } finally {
   1586             resp.close();
   1587         }
   1588         // On failures, log issue and return null
   1589         ExchangeService.log("Provisioning failed for" +
   1590                 (PROVISION_STATUS_PARTIAL.equals(status) ? "PART" : "FULL") + " set");
   1591         return null;
   1592     }
   1593 
   1594     private boolean sendSettings() throws IOException {
   1595         Serializer s = new Serializer();
   1596         s.start(Tags.SETTINGS_SETTINGS);
   1597         s.start(Tags.SETTINGS_DEVICE_INFORMATION).start(Tags.SETTINGS_SET);
   1598         s.data(Tags.SETTINGS_MODEL, Build.MODEL);
   1599         s.data(Tags.SETTINGS_OS, "Android " + Build.VERSION.RELEASE);
   1600         s.data(Tags.SETTINGS_USER_AGENT, USER_AGENT);
   1601         s.end().end().end().done(); // SETTINGS_SET, SETTINGS_DEVICE_INFORMATION, SETTINGS_SETTINGS
   1602         EasResponse resp = sendHttpClientPost("Settings", s.toByteArray());
   1603         try {
   1604             int code = resp.getStatus();
   1605             if (code == HttpStatus.SC_OK) {
   1606                 InputStream is = resp.getInputStream();
   1607                 SettingsParser sp = new SettingsParser(is);
   1608                 return sp.parse();
   1609             }
   1610         } finally {
   1611             resp.close();
   1612         }
   1613         // On failures, simply return false
   1614         return false;
   1615     }
   1616 
   1617     /**
   1618      * Common code to sync E+PIM data
   1619      * @param target an EasMailbox, EasContacts, or EasCalendar object
   1620      */
   1621     public void sync(AbstractSyncAdapter target) throws IOException {
   1622         Mailbox mailbox = target.mMailbox;
   1623 
   1624         boolean moreAvailable = true;
   1625         int loopingCount = 0;
   1626         while (!mStop && (moreAvailable || hasPendingRequests())) {
   1627             // If we have no connectivity, just exit cleanly. ExchangeService will start us up again
   1628             // when connectivity has returned
   1629             if (!hasConnectivity()) {
   1630                 userLog("No connectivity in sync; finishing sync");
   1631                 mExitStatus = EXIT_DONE;
   1632                 return;
   1633             }
   1634 
   1635             // Every time through the loop we check to see if we're still syncable
   1636             if (!target.isSyncable()) {
   1637                 mExitStatus = EXIT_DONE;
   1638                 return;
   1639             }
   1640 
   1641             // Now, handle various requests
   1642             while (true) {
   1643                 Request req = null;
   1644 
   1645                 if (mRequestQueue.isEmpty()) {
   1646                     break;
   1647                 } else {
   1648                     req = mRequestQueue.peek();
   1649                 }
   1650 
   1651                 // Our two request types are PartRequest (loading attachment) and
   1652                 // MeetingResponseRequest (respond to a meeting request)
   1653                 if (req instanceof PartRequest) {
   1654                     TrafficStats.setThreadStatsTag(
   1655                             TrafficFlags.getAttachmentFlags(mContext, mAccount));
   1656                     new AttachmentLoader(this, (PartRequest)req).loadAttachment();
   1657                     TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, mAccount));
   1658                 } else if (req instanceof MeetingResponseRequest) {
   1659                     sendMeetingResponse((MeetingResponseRequest)req);
   1660                 } else if (req instanceof MessageMoveRequest) {
   1661                     messageMoveRequest((MessageMoveRequest)req);
   1662                 }
   1663 
   1664                 // If there's an exception handling the request, we'll throw it
   1665                 // Otherwise, we remove the request
   1666                 mRequestQueue.remove();
   1667             }
   1668 
   1669             // Don't sync if we've got nothing to do
   1670             if (!moreAvailable) {
   1671                 continue;
   1672             }
   1673 
   1674             Serializer s = new Serializer();
   1675 
   1676             String className = target.getCollectionName();
   1677             String syncKey = target.getSyncKey();
   1678             userLog("sync, sending ", className, " syncKey: ", syncKey);
   1679             s.start(Tags.SYNC_SYNC)
   1680                 .start(Tags.SYNC_COLLECTIONS)
   1681                 .start(Tags.SYNC_COLLECTION);
   1682             // The "Class" element is removed in EAS 12.1 and later versions
   1683             if (mProtocolVersionDouble < Eas.SUPPORTED_PROTOCOL_EX2007_SP1_DOUBLE) {
   1684                 s.data(Tags.SYNC_CLASS, className);
   1685             }
   1686             s.data(Tags.SYNC_SYNC_KEY, syncKey)
   1687                 .data(Tags.SYNC_COLLECTION_ID, mailbox.mServerId);
   1688 
   1689             // Start with the default timeout
   1690             int timeout = COMMAND_TIMEOUT;
   1691             boolean initialSync = syncKey.equals("0");
   1692             // EAS doesn't allow GetChanges in an initial sync; sending other options
   1693             // appears to cause the server to delay its response in some cases, and this delay
   1694             // can be long enough to result in an IOException and total failure to sync.
   1695             // Therefore, we don't send any options with the initial sync.
   1696             // Set the truncation amount, body preference, lookback, etc.
   1697             target.sendSyncOptions(mProtocolVersionDouble, s, initialSync);
   1698             if (initialSync) {
   1699                 // Use enormous timeout for initial sync, which empirically can take a while longer
   1700                 timeout = (int)(120 * DateUtils.SECOND_IN_MILLIS);
   1701             }
   1702             // Send our changes up to the server
   1703             if (mUpsyncFailed) {
   1704                 if (Eas.USER_LOG) {
   1705                     LogUtils.d(TAG, "Inhibiting upsync this cycle");
   1706                 }
   1707             } else {
   1708                 target.sendLocalChanges(s);
   1709             }
   1710 
   1711             s.end().end().end().done();
   1712             EasResponse resp = sendHttpClientPost("Sync", new ByteArrayEntity(s.toByteArray()),
   1713                     timeout);
   1714             try {
   1715                 int code = resp.getStatus();
   1716                 if (code == HttpStatus.SC_OK) {
   1717                     // In EAS 12.1, we can get "empty" sync responses, which indicate that there are
   1718                     // no changes in the mailbox; handle that case here
   1719                     // There are two cases here; if we get back a compressed stream (GZIP), we won't
   1720                     // know until we try to parse it (and generate an EmptyStreamException). If we
   1721                     // get uncompressed data, the response will be empty (i.e. have zero length)
   1722                     boolean emptyStream = false;
   1723                     if (!resp.isEmpty()) {
   1724                         InputStream is = resp.getInputStream();
   1725                         try {
   1726                             moreAvailable = target.parse(is);
   1727                             // If we inhibited upsync, we need yet another sync
   1728                             if (mUpsyncFailed) {
   1729                                 moreAvailable = true;
   1730                             }
   1731 
   1732                             if (target.isLooping()) {
   1733                                 loopingCount++;
   1734                                 userLog("** Looping: " + loopingCount);
   1735                                 // After the maximum number of loops, we'll set moreAvailable to
   1736                                 // false and allow the sync loop to terminate
   1737                                 if (moreAvailable && (loopingCount > MAX_LOOPING_COUNT)) {
   1738                                     userLog("** Looping force stopped");
   1739                                     moreAvailable = false;
   1740                                 }
   1741                             } else {
   1742                                 loopingCount = 0;
   1743                             }
   1744 
   1745                             // Cleanup clears out the updated/deleted tables, and we don't want to
   1746                             // do that if our upsync failed; clear the flag otherwise
   1747                             if (!mUpsyncFailed) {
   1748                                 target.cleanup();
   1749                             } else {
   1750                                 mUpsyncFailed = false;
   1751                             }
   1752                         } catch (EmptyStreamException e) {
   1753                             userLog("Empty stream detected in GZIP response");
   1754                             emptyStream = true;
   1755                         } catch (CommandStatusException e) {
   1756                             // TODO 14.1
   1757                             int status = e.mStatus;
   1758                             if (CommandStatus.isNeedsProvisioning(status)) {
   1759                                 mExitStatus = EXIT_SECURITY_FAILURE;
   1760                             } else if (CommandStatus.isDeniedAccess(status)) {
   1761                                 mExitStatus = EXIT_ACCESS_DENIED;
   1762                             } else if (CommandStatus.isTransientError(status)) {
   1763                                 mExitStatus = EXIT_IO_ERROR;
   1764                             } else {
   1765                                 mExitStatus = EXIT_EXCEPTION;
   1766                             }
   1767                             return;
   1768                         }
   1769                     } else {
   1770                         emptyStream = true;
   1771                     }
   1772 
   1773                     if (emptyStream) {
   1774                         // Make sure we get rid of updates/deletes
   1775                         target.cleanup();
   1776                         // If this happens, exit cleanly, and change the interval from push to ping
   1777                         // if necessary
   1778                         userLog("Empty sync response; finishing");
   1779                         if (mMailbox.mSyncInterval == Mailbox.CHECK_INTERVAL_PUSH) {
   1780                             userLog("Changing mailbox from push to ping");
   1781                             ContentValues cv = new ContentValues();
   1782                             cv.put(Mailbox.SYNC_INTERVAL, Mailbox.CHECK_INTERVAL_PING);
   1783                             mContentResolver.update(
   1784                                     ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId),
   1785                                     cv, null, null);
   1786                         }
   1787                         if (mRequestQueue.isEmpty()) {
   1788                             mExitStatus = EXIT_DONE;
   1789                             return;
   1790                         } else {
   1791                             continue;
   1792                         }
   1793                     }
   1794                 } else {
   1795                     userLog("Sync response error: ", code);
   1796                     if (resp.isProvisionError()) {
   1797                         mExitStatus = EXIT_SECURITY_FAILURE;
   1798                     } else if (resp.isAuthError()) {
   1799                         mExitStatus = EXIT_LOGIN_FAILURE;
   1800                     } else {
   1801                         mExitStatus = EXIT_IO_ERROR;
   1802                     }
   1803                     return;
   1804                 }
   1805             } finally {
   1806                 resp.close();
   1807             }
   1808         }
   1809         mExitStatus = EXIT_DONE;
   1810     }
   1811 
   1812     protected boolean setupService() {
   1813         synchronized(getSynchronizer()) {
   1814             mThread = Thread.currentThread();
   1815             android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);
   1816             TAG = mThread.getName();
   1817         }
   1818         // Make sure account and mailbox are always the latest from the database
   1819         mAccount = Account.restoreAccountWithId(mContext, mAccount.mId);
   1820         if (mAccount == null) return false;
   1821         mMailbox = Mailbox.restoreMailboxWithId(mContext, mMailbox.mId);
   1822         if (mMailbox == null) return false;
   1823         HostAuth ha = HostAuth.restoreHostAuthWithId(mContext, mAccount.mHostAuthKeyRecv);
   1824         if (ha == null) return false;
   1825         mHostAddress = ha.mAddress;
   1826         mUserName = ha.mLogin;
   1827         mPassword = ha.mPassword;
   1828 
   1829         try {
   1830             setConnectionParameters(ha);
   1831         } catch (CertificateException e) {
   1832             userLog("Couldn't retrieve certificate for connection");
   1833             return false;
   1834         }
   1835 
   1836         // Set up our protocol version from the Account
   1837         mProtocolVersion = mAccount.mProtocolVersion;
   1838         // If it hasn't been set up, start with default version
   1839         if (mProtocolVersion == null) {
   1840             mProtocolVersion = Eas.DEFAULT_PROTOCOL_VERSION;
   1841         }
   1842         mProtocolVersionDouble = Eas.getProtocolVersionDouble(mProtocolVersion);
   1843 
   1844         // Do checks to address historical policy sets.
   1845         Policy policy = Policy.restorePolicyWithId(mContext, mAccount.mPolicyKey);
   1846         if ((policy != null) && policy.mRequireEncryptionExternal) {
   1847             // External storage encryption is not supported at this time. In a previous release,
   1848             // prior to the system supporting true removable storage on Honeycomb, we accepted
   1849             // this since we emulated external storage on partitions that could be encrypted.
   1850             // If that was set before, we must clear it out now that the system supports true
   1851             // removable storage (which can't be encrypted).
   1852             resetSecurityPolicies();
   1853         }
   1854         return true;
   1855     }
   1856 
   1857     /**
   1858      * Clears out the security policies associated with the account, forcing a provision error
   1859      * and a re-sync of the policy information for the account.
   1860      */
   1861     @SuppressWarnings("deprecation")
   1862     void resetSecurityPolicies() {
   1863         ContentValues cv = new ContentValues();
   1864         cv.put(AccountColumns.SECURITY_FLAGS, 0);
   1865         cv.putNull(AccountColumns.SECURITY_SYNC_KEY);
   1866         long accountId = mAccount.mId;
   1867         mContentResolver.update(ContentUris.withAppendedId(
   1868                 Account.CONTENT_URI, accountId), cv, null, null);
   1869     }
   1870 
   1871     @Override
   1872     public void run() {
   1873         try {
   1874             // Make sure account and mailbox are still valid
   1875             if (!setupService()) return;
   1876             // If we've been stopped, we're done
   1877             if (mStop) return;
   1878 
   1879             // Whether or not we're the account mailbox
   1880             try {
   1881                 mDeviceId = ExchangeService.getDeviceId(mContext);
   1882                 int trafficFlags = TrafficFlags.getSyncFlags(mContext, mAccount);
   1883                 if ((mMailbox == null) || (mAccount == null)) {
   1884                     return;
   1885                 } else {
   1886                     AbstractSyncAdapter target;
   1887                     if (mMailbox.mType == Mailbox.TYPE_CONTACTS) {
   1888                         // ContactsSyncAdapter is gone, and this class is deprecated.
   1889                         // Just leaving this commented out here for reference.
   1890 //                        TrafficStats.setThreadStatsTag(trafficFlags | TrafficFlags.DATA_CONTACTS);
   1891 //                        target = new ContactsSyncAdapter(this);
   1892                         target = null;
   1893                     } else if (mMailbox.mType == Mailbox.TYPE_CALENDAR) {
   1894                         // CalendarSyncAdapter is gone, and this class is deprecated.
   1895                         // Just leaving this commented out here for reference.
   1896 //                        TrafficStats.setThreadStatsTag(trafficFlags | TrafficFlags.DATA_CALENDAR);
   1897 //                        target = new CalendarSyncAdapter(this);
   1898                         target = null;
   1899                     } else {
   1900                         TrafficStats.setThreadStatsTag(trafficFlags | TrafficFlags.DATA_EMAIL);
   1901                         target = new EmailSyncAdapter(this);
   1902                     }
   1903                     // We loop because someone might have put a request in while we were syncing
   1904                     // and we've missed that opportunity...
   1905                     do {
   1906                         if (mRequestTime != 0) {
   1907                             userLog("Looping for user request...");
   1908                             mRequestTime = 0;
   1909                         }
   1910                         sync(target);
   1911                     } while (mRequestTime != 0);
   1912                 }
   1913             } catch (EasAuthenticationException e) {
   1914                 userLog("Caught authentication error");
   1915                 mExitStatus = EXIT_LOGIN_FAILURE;
   1916             } catch (IOException e) {
   1917                 String message = e.getMessage();
   1918                 userLog("Caught IOException: ", (message == null) ? "No message" : message);
   1919                 mExitStatus = EXIT_IO_ERROR;
   1920             } catch (Exception e) {
   1921                 userLog("Uncaught exception in EasSyncService", e);
   1922             } finally {
   1923                 int status;
   1924                 ExchangeService.done(this);
   1925                 if (!mStop) {
   1926                     userLog("Sync finished");
   1927                     switch (mExitStatus) {
   1928                         case EXIT_IO_ERROR:
   1929                             status = EmailServiceStatus.CONNECTION_ERROR;
   1930                             break;
   1931                         case EXIT_DONE:
   1932                             status = EmailServiceStatus.SUCCESS;
   1933                             ContentValues cv = new ContentValues();
   1934                             cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
   1935                             String s = "S" + mSyncReason + ':' + status + ':' + mChangeCount;
   1936                             cv.put(Mailbox.SYNC_STATUS, s);
   1937                             mContentResolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI,
   1938                                     mMailboxId), cv, null, null);
   1939                             break;
   1940                         case EXIT_LOGIN_FAILURE:
   1941                             status = EmailServiceStatus.LOGIN_FAILED;
   1942                             break;
   1943                         case EXIT_SECURITY_FAILURE:
   1944                             status = EmailServiceStatus.SECURITY_FAILURE;
   1945                             // Ask for a new folder list. This should wake up the account mailbox; a
   1946                             // security error in account mailbox should start provisioning
   1947                             ExchangeService.reloadFolderList(mContext, mAccount.mId, true);
   1948                             break;
   1949                         case EXIT_ACCESS_DENIED:
   1950                             status = EmailServiceStatus.ACCESS_DENIED;
   1951                             break;
   1952                         default:
   1953                             status = EmailServiceStatus.REMOTE_EXCEPTION;
   1954                             errorLog("Sync ended due to an exception.");
   1955                             break;
   1956                     }
   1957                 } else {
   1958                     userLog("Stopped sync finished.");
   1959                     status = EmailServiceStatus.SUCCESS;
   1960                 }
   1961 
   1962                 // Make sure ExchangeService knows about this
   1963                 ExchangeService.kick("sync finished");
   1964             }
   1965         } catch (ProviderUnavailableException e) {
   1966             LogUtils.e(TAG, "EmailProvider unavailable; sync ended prematurely");
   1967         }
   1968     }
   1969 }
   1970