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