Finish up IMAP ID implementation

* scrub all external strings to keep them compliant for IMAP protocol
* move Build.MODEL to x-android-device-model
* send x-android-mobile-net-operator
* send AGUID
* unit tests for above
* retrieve providers from VendorPolicyLoader

Bug: 2332183
This commit is contained in:
Andrew Stadler 2010-02-01 15:53:46 -08:00
parent b89bc81f54
commit ecb1af8041
6 changed files with 388 additions and 84 deletions

View File

@ -21,6 +21,8 @@ import android.content.SharedPreferences;
import android.net.Uri;
import android.util.Log;
import java.util.UUID;
public class Preferences {
// Preferences file
@ -33,6 +35,7 @@ public class Preferences {
private static final String ENABLE_SENSITIVE_LOGGING = "enableSensitiveLogging";
private static final String ENABLE_EXCHANGE_LOGGING = "enableExchangeLogging";
private static final String ENABLE_EXCHANGE_FILE_LOGGING = "enableExchangeFileLogging";
private static final String DEVICE_UID = "deviceUID";
private static Preferences preferences;
@ -161,6 +164,20 @@ public class Preferences {
return mSharedPreferences.getBoolean(ENABLE_EXCHANGE_FILE_LOGGING, false);
}
/**
* Generate a new "device UID". This is local to Email app only, to prevent possibility
* of correlation with any other user activities in any other apps.
* @return a persistent, unique ID
*/
public synchronized String getDeviceUID() {
String result = mSharedPreferences.getString(DEVICE_UID, null);
if (result == null) {
result = UUID.randomUUID().toString();
mSharedPreferences.edit().putString(DEVICE_UID, result).commit();
}
return result;
}
public void save() {
}

View File

@ -16,6 +16,8 @@
package com.android.email;
import com.android.email.activity.setup.AccountSetupBasics.Provider;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager.NameNotFoundException;
@ -23,6 +25,8 @@ import android.os.Bundle;
import android.util.Log;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
/**
* A bridge class to the email vendor policy apk.
@ -40,7 +44,19 @@ public class VendorPolicyLoader {
private static final String GET_POLICY_METHOD = "getPolicy";
private static final Class<?>[] ARGS = new Class<?>[] {String.class, Bundle.class};
// call keys and i/o bundle keys
// when there is only one parameter or return value, use call key
private static final String USE_ALTERNATE_EXCHANGE_STRINGS = "useAlternateExchangeStrings";
private static final String GET_IMAP_ID = "getImapId";
private static final String GET_IMAP_ID_USER = "getImapId.user";
private static final String GET_IMAP_ID_HOST = "getImapId.host";
private static final String GET_IMAP_ID_CAPA = "getImapId.capabilities";
private static final String FIND_PROVIDER = "findProvider";
private static final String FIND_PROVIDER_IN_URI = "findProvider.inUri";
private static final String FIND_PROVIDER_IN_USER = "findProvider.inUser";
private static final String FIND_PROVIDER_OUT_URI = "findProvider.outUri";
private static final String FIND_PROVIDER_OUT_USER = "findProvider.outUser";
private static final String FIND_PROVIDER_NOTE = "findProvider.note";
/** Singleton instance */
private static VendorPolicyLoader sInstance;
@ -120,9 +136,77 @@ public class VendorPolicyLoader {
/**
* Returns true if alternate exchange descriptive text is required.
*
* Vendor function:
* Select: USE_ALTERNATE_EXCHANGE_STRINGS
* Params: none
* Result: USE_ALTERNATE_EXCHANGE_STRINGS (boolean)
*/
public boolean useAlternateExchangeStrings() {
return getPolicy(USE_ALTERNATE_EXCHANGE_STRINGS, null)
.getBoolean(USE_ALTERNATE_EXCHANGE_STRINGS, false);
}
/**
* Returns additional key/value pairs for the IMAP ID string.
*
* Vendor function:
* Select: GET_IMAP_ID
* Params: GET_IMAP_ID_USER (String)
* GET_IMAP_ID_HOST (String)
* GET_IMAP_ID_CAPABILITIES (String)
* Result: GET_IMAP_ID (String)
*
* @param userName the server that is being contacted (e.g. "imap.server.com")
* @param host the server that is being contacted (e.g. "imap.server.com")
* @param reported capabilities, if known. null is OK
* @return zero or more key/value pairs, quoted and delimited by spaces. If there is
* nothing to add, return null.
*/
public String getImapIdValues(String userName, String host, String capabilities) {
Bundle params = new Bundle();
params.putString(GET_IMAP_ID_USER, userName);
params.putString(GET_IMAP_ID_HOST, host);
params.putString(GET_IMAP_ID_CAPA, capabilities);
String result = getPolicy(GET_IMAP_ID, params).getString(GET_IMAP_ID);
return result;
}
/**
* Returns provider setup information for a given email address
*
* Vendor function:
* Select: FIND_PROVIDER
* Param: FIND_PROVIDER (String)
* Result: FIND_PROVIDER_IN_URI
* FIND_PROVIDER_IN_USER
* FIND_PROVIDER_OUT_URI
* FIND_PROVIDER_OUT_USER
* FIND_PROVIDER_NOTE
*
* @param domain The domain portion of the user's email address
* @return suitable Provider definition, or null if no match found
*/
public Provider findProviderForDomain(String domain) {
Bundle params = new Bundle();
params.putString(FIND_PROVIDER, domain);
Bundle out = getPolicy(FIND_PROVIDER, params);
if (out != null) {
try {
Provider p = new Provider();
p.id = null;
p.label = null;
p.domain = domain;
p.incomingUriTemplate = new URI(out.getString(FIND_PROVIDER_IN_URI));
p.incomingUsernameTemplate = out.getString(FIND_PROVIDER_IN_USER);
p.outgoingUriTemplate = new URI(out.getString(FIND_PROVIDER_OUT_URI));
p.outgoingUsernameTemplate = out.getString(FIND_PROVIDER_OUT_USER);
p.note = out.getString(FIND_PROVIDER_NOTE);
return p;
} catch (URISyntaxException e) {
Log.d(Email.LOG_TAG, "uri exception while vendor policy loads " + domain);
}
}
return null;
}
}

View File

@ -534,8 +534,9 @@ public class AccountSetupBasics extends Activity
/**
* Search the list of known Email providers looking for one that matches the user's email
* domain. We look in providers_product.xml first, followed by the entries in
* platform providers.xml. This provides a nominal override capability.
* domain. We check for vendor supplied values first, then we look in providers_product.xml
* first, finally by the entries in platform providers.xml. This provides a nominal override
* capability.
*
* A match is defined as any provider entry for which the "domain" attribute matches.
*
@ -543,7 +544,10 @@ public class AccountSetupBasics extends Activity
* @return suitable Provider definition, or null if no match found
*/
private Provider findProviderForDomain(String domain) {
Provider p = findProviderForDomain(domain, R.xml.providers_product);
Provider p = VendorPolicyLoader.getInstance(this).findProviderForDomain(domain);
if (p == null) {
p = findProviderForDomain(domain, R.xml.providers_product);
}
if (p == null) {
p = findProviderForDomain(domain, R.xml.providers);
}
@ -597,23 +601,16 @@ public class AccountSetupBasics extends Activity
return null;
}
static class Provider implements Serializable {
public static class Provider implements Serializable {
private static final long serialVersionUID = 8511656164616538989L;
public String id;
public String label;
public String domain;
public URI incomingUriTemplate;
public String incomingUsernameTemplate;
public URI outgoingUriTemplate;
public String outgoingUsernameTemplate;
public String note;
}
}

View File

@ -17,7 +17,10 @@
package com.android.email.mail.store;
import com.android.email.Email;
import com.android.email.Preferences;
import com.android.email.Utility;
import com.android.email.VendorPolicyLoader;
import com.android.email.codec.binary.Base64;
import com.android.email.mail.AuthenticationFailedException;
import com.android.email.mail.CertificateValidationException;
import com.android.email.mail.FetchProfile;
@ -43,6 +46,7 @@ import com.beetstra.jutf7.CharsetProvider;
import android.content.Context;
import android.os.Build;
import android.telephony.TelephonyManager;
import android.util.Config;
import android.util.Log;
@ -54,12 +58,15 @@ import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Pattern;
import javax.net.ssl.SSLException;
@ -174,7 +181,7 @@ public class ImapStore extends Store {
mModifiedUtf7Charset = new CharsetProvider().charsetForName("X-RFC-3501");
// Assign user-agent string (for RFC2971 ID command)
String mUserAgent = getImapId(context);
String mUserAgent = getImapId(context, mUsername, mRootTransport.getHost());
if (mUserAgent != null) {
mIdPhrase = "ID (" + mUserAgent + ")";
} else if (DEBUG_FORCE_SEND_ID) {
@ -202,61 +209,142 @@ public class ImapStore extends Store {
* because some connections may append additional values.
*
* The following IMAP ID keys may be included:
* name Android package name of the program
* os "android"
* os-version "version; model; build-id"
* vendor Vendor of the client/server
* name Android package name of the program
* os "android"
* os-version "version; model; build-id"
* vendor Vendor of the client/server
* x-android-device-model Model (only revealed if release build)
* x-android-net-operator Mobile network operator (if known)
* AGUID A device+account UID
*
* In addition, a vendor policy .apk can append key/value pairs.
*
* @param userName the username of the account
* @param host the host (server) of the account
* @return a String for use in an IMAP ID message.
*/
public String getImapId(Context context) {
synchronized (Email.class) {
public String getImapId(Context context, String userName, String host) {
// The first section is global to all IMAP connections, and generates the fixed
// values in any IMAP ID message
synchronized (ImapStore.class) {
if (sImapId == null) {
// "name" "com.android.email"
StringBuffer sb = new StringBuffer("\"name\" \"");
sb.append(context.getPackageName());
sb.append("\"");
String networkOperator = TelephonyManager.getDefault().getNetworkOperatorName();
if (networkOperator == null) networkOperator = "";
// "os" "android"
sb.append(" \"os\" \"android\"");
// "os-version" "version; model; build-id"
sb.append(" \"os-version\" \"");
final String version = Build.VERSION.RELEASE;
if (version.length() > 0) {
sb.append(version);
} else {
// default to "1.0"
sb.append("1.0");
}
// add the model (on release builds only)
if ("REL".equals(Build.VERSION.CODENAME)) {
final String model = Build.MODEL;
if (model.length() > 0) {
sb.append("; ");
sb.append(model);
}
}
// add the build ID or build #
final String id = Build.ID;
if (id.length() > 0) {
sb.append("; ");
sb.append(id);
}
sb.append("\"");
// "vendor" "the vendor"
final String vendor = Build.MANUFACTURER;
if (vendor.length() > 0) {
sb.append(" \"vendor\" \"");
sb.append(vendor);
sb.append("\"");
}
sImapId = sb.toString();
sImapId = makeCommonImapId(context.getPackageName(), Build.VERSION.RELEASE,
Build.VERSION.CODENAME, Build.MODEL, Build.ID, Build.MANUFACTURER,
networkOperator);
}
}
return sImapId;
// This section is per Store, and adds in a dynamic elements like UID's.
// We don't cache the result of this work, because the caller does anyway.
StringBuilder id = new StringBuilder(sImapId);
// Optionally add any vendor-supplied id keys
String vendorId =
VendorPolicyLoader.getInstance(context).getImapIdValues(userName, host, null);
if (vendorId != null) {
id.append(' ');
id.append(vendorId);
}
// Generate a UID that mixes a "stable" device UID with the email address
try {
String devUID = Preferences.getPreferences(context).getDeviceUID();
MessageDigest messageDigest;
messageDigest = MessageDigest.getInstance("SHA-1");
messageDigest.update(userName.getBytes());
messageDigest.update(devUID.getBytes());
byte[] uid = messageDigest.digest();
String hexUid = new String(new Base64().encode(uid));
id.append(" \"AGUID\" \"");
id.append(hexUid);
id.append('\"');
} catch (NoSuchAlgorithmException e) {
Log.d(Email.LOG_TAG, "couldn't obtain SHA-1 hash for device UID");
}
return id.toString();
}
/**
* Helper function that actually builds the static part of the IMAP ID string. This is
* separated from getImapId for testability. There is no escaping or encoding in IMAP ID so
* any rogue chars must be filtered here.
*
* @param packageName context.getPackageName()
* @param version Build.VERSION.RELEASE
* @param codeName Build.VERSION.CODENAME
* @param model Build.MODEL
* @param id Build.ID
* @param vendor Build.MANUFACTURER
* @param networkOperator TelephonyManager.getNetworkOperatorName()
* @return the static (never changes) portion of the IMAP ID
*/
/* package */ String makeCommonImapId(String packageName, String version,
String codeName, String model, String id, String vendor, String networkOperator) {
// Before building up IMAP ID string, pre-filter the input strings for "legal" chars
// This is using a fairly arbitrary char set intended to pass through most reasonable
// version, model, and vendor strings: a-z A-Z 0-9 - _ + = ; : . , / <space>
// The most important thing is *not* to pass parens, quotes, or CRLF, which would break
// the format of the IMAP ID list.
Pattern p = Pattern.compile("[^a-zA-Z0-9-_\\+=;:\\.,/ ]");
packageName = p.matcher(packageName).replaceAll("");
version = p.matcher(version).replaceAll("");
codeName = p.matcher(codeName).replaceAll("");
model = p.matcher(model).replaceAll("");
id = p.matcher(id).replaceAll("");
vendor = p.matcher(vendor).replaceAll("");
networkOperator = p.matcher(networkOperator).replaceAll("");
// "name" "com.android.email"
StringBuffer sb = new StringBuffer("\"name\" \"");
sb.append(packageName);
sb.append("\"");
// "os" "android"
sb.append(" \"os\" \"android\"");
// "os-version" "version; build-id"
sb.append(" \"os-version\" \"");
if (version.length() > 0) {
sb.append(version);
} else {
// default to "1.0"
sb.append("1.0");
}
// add the build ID or build #
if (id.length() > 0) {
sb.append("; ");
sb.append(id);
}
sb.append("\"");
// "vendor" "the vendor"
if (vendor.length() > 0) {
sb.append(" \"vendor\" \"");
sb.append(vendor);
sb.append("\"");
}
// "x-android-device-model" the device model (on release builds only)
if ("REL".equals(codeName)) {
if (model.length() > 0) {
sb.append(" \"x-android-device-model\" \"");
sb.append(model);
sb.append("\"");
}
}
// "x-android-mobile-net-operator" "name of network operator"
if (networkOperator.length() > 0) {
sb.append(" \"x-android-mobile-net-operator\" \"");
sb.append(networkOperator);
sb.append("\"");
}
return sb.toString();
}

View File

@ -20,6 +20,8 @@ import android.content.Context;
import android.os.Bundle;
import android.test.AndroidTestCase;
import java.util.HashMap;
public class VendorPolicyLoaderTest extends AndroidTestCase {
/**
* Test for the case where the helper package doesn't exist.
@ -86,7 +88,7 @@ public class VendorPolicyLoaderTest extends AndroidTestCase {
assertNull(MockVendorPolicy.passedPolicy);
}
public static class MockVendorPolicy {
private static class MockVendorPolicy {
public static String passedPolicy;
public static Bundle passedBundle;
public static Bundle mockResult;
@ -97,4 +99,40 @@ public class VendorPolicyLoaderTest extends AndroidTestCase {
return mockResult;
}
}
/**
* Test that any vendor policy that happens to be installed returns legal values
* for getImapIdValues() per its API.
*
* Note, in most cases very little will happen in this test, because there is
* no vendor policy package. Most of this test exists to test a vendor policy
* package itself, to make sure that its API returns reasonable values.
*/
public void testGetImapIdValues() {
VendorPolicyLoader pl = VendorPolicyLoader.getInstance(getContext());
String id = pl.getImapIdValues("user-name", "server.yahoo.com",
"IMAP4rev1 STARTTLS AUTH=GSSAPI");
// null is a reasonable result
if (id == null) return;
// if non-null, basic sanity checks on format
assertEquals("\"", id.charAt(0));
assertEquals("\"", id.charAt(id.length()-1));
// see if we can break it up properly
String[] elements = id.split("\"");
assertEquals(0, elements.length % 4);
for (int i = 0; i < elements.length; ) {
// Because we split at quotes, we expect to find:
// [i] = null or one or more spaces
// [i+1] = key
// [i+2] = one or more spaces
// [i+3] = value
// Here are some incomplete checks of the above
assertTrue(elements[i] == null || elements[i].startsWith(" "));
assertTrue(elements[i+1].charAt(0) != ' ');
assertTrue(elements[i+2].startsWith(" "));
assertTrue(elements[i+3].charAt(0) != ' ');
i += 4;
}
}
}

View File

@ -16,7 +16,6 @@
package com.android.email.mail.store;
import com.android.email.Email;
import com.android.email.mail.FetchProfile;
import com.android.email.mail.Flag;
import com.android.email.mail.Folder;
@ -31,8 +30,8 @@ import com.android.email.mail.internet.MimeUtility;
import com.android.email.mail.transport.MockTransport;
import android.test.AndroidTestCase;
import android.test.MoreAsserts;
import android.test.suitebuilder.annotation.SmallTest;
import android.util.Log;
import java.util.ArrayList;
import java.util.Date;
@ -93,40 +92,121 @@ public class ImapStoreUnitTests extends AndroidTestCase {
/**
* Test the generation of the IMAP ID keys
*
* Since this is build-specific, we mostly just ensure that the correct strings
* are being generated, and non-empty, and (if possible) look for expected formatting.
*/
public void testImapId() {
String id = mStore.getImapId(getContext());
public void testImapIdBasic() {
// First test looks at operation of the outer API - we don't control any of the
// values; Just look for basic results.
// Strings we'll expect to find:
// name Android package name of the program
// os "android"
// os-version "version; build-id"
// vendor Vendor of the client/server
// x-android-device-model Model (Optional, so not tested here)
// x-android-net-operator Carrier (Unreliable, so not tested here)
// AGUID A device+account UID
String id = mStore.getImapId(getContext(), "user-name", "host-name");
HashMap<String, String> map = tokenizeImapId(id);
assertEquals(getContext().getPackageName(), map.get("name"));
assertEquals("android", map.get("os"));
assertNotNull(map.get("os-version"));
assertNotNull(map.get("vendor"));
assertNotNull(map.get("AGUID"));
// Next, use the inner API to confirm operation of a couple of
// variants for release and non-release devices.
// simple API check - non-REL codename, non-empty version
id = mStore.makeCommonImapId("packageName", "version", "codeName",
"model", "id", "vendor", "network-operator");
map = tokenizeImapId(id);
assertEquals("packageName", map.get("name"));
assertEquals("android", map.get("os"));
assertEquals("version; id", map.get("os-version"));
assertEquals("vendor", map.get("vendor"));
assertEquals(null, map.get("x-android-device-model"));
assertEquals("network-operator", map.get("x-android-mobile-net-operator"));
assertEquals(null, map.get("AGUID"));
// simple API check - codename is REL, so use model name.
// also test empty version => 1.0 and empty network operator
id = mStore.makeCommonImapId("packageName", "", "REL",
"model", "id", "vendor", "");
map = tokenizeImapId(id);
assertEquals("packageName", map.get("name"));
assertEquals("android", map.get("os"));
assertEquals("1.0; id", map.get("os-version"));
assertEquals("vendor", map.get("vendor"));
assertEquals("model", map.get("x-android-device-model"));
assertEquals(null, map.get("x-android-mobile-net-operator"));
assertEquals(null, map.get("AGUID"));
}
/**
* Test of the internal generator for IMAP ID strings, specifically looking for proper
* filtering of illegal values. This is required because we cannot necessarily trust
* the external sources of some of this data (e.g. release labels).
*
* The (somewhat arbitrary) legal values are: a-z A-Z 0-9 - _ + = ; : . , / <space>
* The most important goal of the filters is to keep out control chars, (, ), and "
*/
public void testImapIdFiltering() {
String id = mStore.makeCommonImapId("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
"0123456789", "codeName",
"model", "-_+=;:.,// ",
"v(e)n\"d\ro\nr", // look for bad chars stripped out, leaving OK chars
"()\""); // look for bad chars stripped out, leaving nothing
HashMap<String, String> map = tokenizeImapId(id);
assertEquals("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", map.get("name"));
assertEquals("0123456789; -_+=;:.,// ", map.get("os-version"));
assertEquals("vendor", map.get("vendor"));
assertNull(map.get("x-android-mobile-net-operator"));
}
/**
* Test that IMAP ID uid's are per-username
*/
public void testImapIdDeviceId() throws MessagingException {
ImapStore store1a = (ImapStore) ImapStore.newInstance("imap://user1:password@server:999",
getContext(), null);
ImapStore store1b = (ImapStore) ImapStore.newInstance("imap://user1:password@server:999",
getContext(), null);
ImapStore store2 = (ImapStore) ImapStore.newInstance("imap://user2:password@server:999",
getContext(), null);
String id1a = mStore.getImapId(getContext(), "user1", "host-name");
String id1b = mStore.getImapId(getContext(), "user1", "host-name");
String id2 = mStore.getImapId(getContext(), "user2", "host-name");
String uid1a = tokenizeImapId(id1a).get("AGUID");
String uid1b = tokenizeImapId(id1b).get("AGUID");
String uid2 = tokenizeImapId(id2).get("AGUID");
assertEquals(uid1a, uid1b);
MoreAsserts.assertNotEqual(uid1a, uid2);
}
/**
* Helper to break an IMAP ID string into keys & values
* @param id the IMAP Id string (the part inside the parens)
* @return a map of key/value pairs
*/
private HashMap<String, String> tokenizeImapId(String id) {
// Instead of a true tokenizer, we'll use double-quote as the split.
// We can's use " " because there may be spaces inside the values.
String[] elements = id.split("\"");
HashMap<String, String> map = new HashMap<String, String>();
for (int i = 0; i < elements.length; ) {
// Because we split at quotes, we expect to find:
// [i] = null
// [i] = null or one or more spaces
// [i+1] = key
// [i+2] = one or more spaces
// [i+3] = value
map.put(elements[i+1], elements[i+3]);
i += 4;
}
// Strings we'll expect to find:
// name Android package name of the program
// os "android"
// os-version "version; model; build-id"
// vendor Vendor of the client/server
String name = map.get("name");
assertEquals(getContext().getPackageName(), name);
String os = map.get("os");
assertEquals("android", os);
String osversion = map.get("os-version");
assertNotNull(osversion);
String vendor = map.get("vendor");
assertNotNull(vendor);
return map;
}
/**