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