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