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