• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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