();
+ int tcnt = 0;
+ StringBuilder tsb = new StringBuilder("uid fetch ");
+ for (tcnt = 0; tcnt < HEADER_BATCH_COUNT && idx <= cnt; tcnt++, idx++) {
+ // Load most recent first
+ if (tcnt > 0)
+ tsb.append(',');
+ tsb.append(loadList.get(cnt - idx));
+ }
+ tsb.append(" (uid internaldate flags envelope bodystructure)");
+ tag = writeCommand(mWriter, tsb.toString());
+ if (readResponse(mReader, tag, "FETCH").equals(IMAP_OK)) {
+ // Create message and store
+ for (int j = 0; j < tcnt; j++) {
+ Message msg = createMessage(mImapResponse.get(j));
+ tmsgList.add(msg);
+ }
+ saveNewMessages(tmsgList);
+ }
+
+ fetchMessageData();
+ loadedSome = true;
+ }
+ // TODO: Use loader to watch for changes on unloaded body cursor
+ if (!loadedSome) {
+ fetchMessageData();
+ }
+
+ // Reflect server deletions on device; do them all at once
+ processServerDeletes(deleteList);
+
+ handleLocalUpdates();
+
+ handleLocalDeletes();
+
+ reconcileState(getUnreadUidList(), since, "UNREAD", "unseen",
+ MessageColumns.FLAG_READ, true);
+ reconcileState(getFlaggedUidList(), since, "FLAGGED", "flagged",
+ MessageColumns.FLAG_FAVORITE, false);
+
+ // We're done if not pushing...
+ if (mMailbox.mSyncInterval != Mailbox.CHECK_INTERVAL_PUSH) {
+ mExitStatus = EXIT_DONE;
+ return;
+ }
+
+ // If new requests have come in, process them
+ if (mIsServiceRequestPending)
+ continue;
+
+ idle();
+ }
+
+ } finally {
+ if (mSocket != null) {
+ try {
+ mSocket.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void run() {
+ try {
+ // If we've been stopped, we're done
+ if (mStop) return;
+
+ // Whether or not we're the account mailbox
+ try {
+ if ((mMailbox == null) || (mAccount == null)) {
+ return;
+ } else {
+ int trafficFlags = TrafficFlags.getSyncFlags(mContext, mAccount);
+ TrafficStats.setThreadStatsTag(trafficFlags | TrafficFlags.DATA_EMAIL);
+
+ // We loop because someone might have put a request in while we were syncing
+ // and we've missed that opportunity...
+ do {
+ if (mRequestTime != 0) {
+ userLog("Looping for user request...");
+ mRequestTime = 0;
+ }
+ if (mSyncReason >= Imap2SyncManager.SYNC_CALLBACK_START) {
+ try {
+ Imap2SyncManager.callback().syncMailboxStatus(mMailboxId,
+ EmailServiceStatus.IN_PROGRESS, 0);
+ } catch (RemoteException e1) {
+ // Don't care if this fails
+ }
+ }
+ sync();
+ } while (mRequestTime != 0);
+ }
+ } catch (IOException e) {
+ String message = e.getMessage();
+ userLog("Caught IOException: ", (message == null) ? "No message" : message);
+ mExitStatus = EXIT_IO_ERROR;
+ } catch (Exception e) {
+ userLog("Uncaught exception in EasSyncService", e);
+ } finally {
+ int status;
+ Imap2SyncManager.done(this);
+ if (!mStop) {
+ userLog("Sync finished");
+ switch (mExitStatus) {
+ case EXIT_IO_ERROR:
+ status = EmailServiceStatus.CONNECTION_ERROR;
+ break;
+ case EXIT_DONE:
+ status = EmailServiceStatus.SUCCESS;
+ ContentValues cv = new ContentValues();
+ cv.put(Mailbox.SYNC_TIME, System.currentTimeMillis());
+ String s = "S" + mSyncReason + ':' + status + ':' + mChangeCount;
+ cv.put(Mailbox.SYNC_STATUS, s);
+ mContext.getContentResolver().update(
+ ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailboxId),
+ cv, null, null);
+ break;
+ case EXIT_LOGIN_FAILURE:
+ status = EmailServiceStatus.LOGIN_FAILED;
+ break;
+ default:
+ status = EmailServiceStatus.REMOTE_EXCEPTION;
+ errorLog("Sync ended due to an exception.");
+ break;
+ }
+ } else {
+ userLog("Stopped sync finished.");
+ status = EmailServiceStatus.SUCCESS;
+ }
+
+ // Send a callback (doesn't matter how the sync was started)
+ try {
+ // Unless the user specifically asked for a sync, we don't want to report
+ // connection issues, as they are likely to be transient. In this case, we
+ // simply report success, so that the progress indicator terminates without
+ // putting up an error banner
+ //***
+ if (mSyncReason != Imap2SyncManager.SYNC_UI_REQUEST &&
+ status == EmailServiceStatus.CONNECTION_ERROR) {
+ status = EmailServiceStatus.SUCCESS;
+ }
+ Imap2SyncManager.callback().syncMailboxStatus(mMailboxId, status, 0);
+ } catch (RemoteException e1) {
+ // Don't care if this fails
+ }
+
+ // Make sure ExchangeService knows about this
+ Imap2SyncManager.kick("sync finished");
+ }
+ } catch (ProviderUnavailableException e) {
+ Log.e(TAG, "EmailProvider unavailable; sync ended prematurely");
+ }
+ }
+
+ private Socket getSocket(HostAuth hostAuth) throws CertificateValidationException, IOException {
+ Socket socket;
+ try {
+ boolean ssl = (hostAuth.mFlags & HostAuth.FLAG_SSL) != 0;
+ boolean trust = (hostAuth.mFlags & HostAuth.FLAG_TRUST_ALL) != 0;
+ SocketAddress socketAddress = new InetSocketAddress(hostAuth.mAddress, hostAuth.mPort);
+ if (ssl) {
+ socket = SSLUtils.getSSLSocketFactory(trust).createSocket();
+ } else {
+ socket = new Socket();
+ }
+ socket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
+ // After the socket connects to an SSL server, confirm that the hostname is as expected
+ if (ssl && !trust) {
+ verifyHostname(socket, hostAuth.mAddress);
+ }
+ } catch (SSLException e) {
+ errorLog(e.toString());
+ throw new CertificateValidationException(e.getMessage(), e);
+ }
+ return socket;
+ }
+
+ /**
+ * Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this
+ * service but is not in the public API.
+ *
+ * Verify the hostname of the certificate used by the other end of a
+ * connected socket. You MUST call this if you did not supply a hostname
+ * to SSLCertificateSocketFactory.createSocket(). It is harmless to call this method
+ * redundantly if the hostname has already been verified.
+ *
+ * Wildcard certificates are allowed to verify any matching hostname,
+ * so "foo.bar.example.com" is verified if the peer has a certificate
+ * for "*.example.com".
+ *
+ * @param socket An SSL socket which has been connected to a server
+ * @param hostname The expected hostname of the remote server
+ * @throws IOException if something goes wrong handshaking with the server
+ * @throws SSLPeerUnverifiedException if the server cannot prove its identity
+ */
+ private void verifyHostname(Socket socket, String hostname) throws IOException {
+ // The code at the start of OpenSSLSocketImpl.startHandshake()
+ // ensures that the call is idempotent, so we can safely call it.
+ SSLSocket ssl = (SSLSocket) socket;
+ ssl.startHandshake();
+
+ SSLSession session = ssl.getSession();
+ if (session == null) {
+ throw new SSLException("Cannot verify SSL socket without session");
+ }
+ // TODO: Instead of reporting the name of the server we think we're connecting to,
+ // we should be reporting the bad name in the certificate. Unfortunately this is buried
+ // in the verifier code and is not available in the verifier API, and extracting the
+ // CN & alts is beyond the scope of this patch.
+ if (!HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, session)) {
+ throw new SSLPeerUnverifiedException(
+ "Certificate hostname not useable for server: " + hostname);
+ }
+ }
+}
diff --git a/imap2/src/com/android/imap2/ImapId.java b/imap2/src/com/android/imap2/ImapId.java
new file mode 100644
index 000000000..f94a46546
--- /dev/null
+++ b/imap2/src/com/android/imap2/ImapId.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.imap2;
+
+import android.content.Context;
+import android.os.Build;
+import android.telephony.TelephonyManager;
+import android.util.Base64;
+import android.util.Log;
+
+import com.android.emailcommon.Device;
+import com.android.emailcommon.Logging;
+import com.android.emailcommon.VendorPolicyLoader;
+import com.google.common.annotations.VisibleForTesting;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.regex.Pattern;
+
+public class ImapId {
+ private static String sImapId;
+
+ /**
+ * Return, or create and return, an string suitable for use in an IMAP ID message.
+ * This is constructed similarly to the way the browser sets up its user-agent strings.
+ * See RFC 2971 for more details. The output of this command will be a series of key-value
+ * pairs delimited by spaces (there is no point in returning a structured result because
+ * this will be sent as-is to the IMAP server). No tokens, parenthesis or "ID" are included,
+ * 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
+ * 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
+ * @param capabilities a list of the capabilities from the server
+ * @return a String for use in an IMAP ID message.
+ */
+ public static String getImapId(Context context, String userName, String host,
+ String capabilities) {
+ // The first section is global to all IMAP connections, and generates the fixed
+ // values in any IMAP ID message
+ synchronized (ImapId.class) {
+ if (sImapId == null) {
+ TelephonyManager tm =
+ (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ String networkOperator = tm.getNetworkOperatorName();
+ if (networkOperator == null) networkOperator = "";
+
+ sImapId = makeCommonImapId(context.getPackageName(), Build.VERSION.RELEASE,
+ Build.VERSION.CODENAME, Build.MODEL, Build.ID, Build.MANUFACTURER,
+ networkOperator);
+ }
+ }
+
+ // 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,
+ capabilities);
+ if (vendorId != null) {
+ id.append(' ');
+ id.append(vendorId);
+ }
+
+ // Generate a UID that mixes a "stable" device UID with the email address
+ try {
+ String devUID = Device.getConsistentDeviceId(context);
+ MessageDigest messageDigest;
+ messageDigest = MessageDigest.getInstance("SHA-1");
+ messageDigest.update(userName.getBytes());
+ messageDigest.update(devUID.getBytes());
+ byte[] uid = messageDigest.digest();
+ String hexUid = Base64.encodeToString(uid, Base64.NO_WRAP);
+ id.append(" \"AGUID\" \"");
+ id.append(hexUid);
+ id.append('\"');
+ } catch (NoSuchAlgorithmException e) {
+ Log.d(Logging.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
+ */
+ @VisibleForTesting
+ static 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 - _ + = ; : . , /
+ // 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();
+ }
+
+}
diff --git a/imap2/src/com/android/imap2/ImapInputStream.java b/imap2/src/com/android/imap2/ImapInputStream.java
new file mode 100644
index 000000000..1730a117d
--- /dev/null
+++ b/imap2/src/com/android/imap2/ImapInputStream.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.imap2;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class ImapInputStream extends FilterInputStream {
+
+ public ImapInputStream(InputStream in) {
+ super(in);
+ }
+
+ public String readLine () throws IOException {
+ StringBuilder sb = new StringBuilder();
+ while (true) {
+ int b = read();
+ // Line ends with \n; ignore \r
+ // I'm not sure this is the right thing with a raw \r (no \n following)
+ if (b < 0)
+ throw new IOException("Socket closed in readLine");
+ if (b == '\n')
+ return sb.toString();
+ else if (b != '\r') {
+ sb.append((char)b);
+ }
+ }
+ }
+
+ public boolean ready () throws IOException {
+ return this.available() > 0;
+ }
+}
diff --git a/imap2/src/com/android/imap2/Parser.java b/imap2/src/com/android/imap2/Parser.java
new file mode 100644
index 000000000..4eb809981
--- /dev/null
+++ b/imap2/src/com/android/imap2/Parser.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.imap2;
+
+public class Parser {
+ String str;
+ int pos;
+ int len;
+ static final String white = "\r\n \t";
+
+ public Parser (String _str) {
+ str = _str;
+ pos = 0;
+ len = str.length();
+ }
+
+ public Parser (String _str, int start) {
+ str = _str;
+ pos = start;
+ len = str.length();
+ }
+
+ public void skipWhite () {
+ while ((pos < len) && white.indexOf(str.charAt(pos)) >= 0)
+ pos++;
+ }
+
+ public String parseAtom () {
+ skipWhite();
+ int start = pos;
+ while ((pos < len) && white.indexOf(str.charAt(pos)) < 0)
+ pos++;
+ if (pos > start)
+ return str.substring(start, pos);
+ return null;
+ }
+
+ public char nextChar () {
+ if (pos >= len)
+ return 0;
+ else
+ return str.charAt(pos++);
+ }
+
+ public char peekChar () {
+ if (pos >= len)
+ return 0;
+ else
+ return str.charAt(pos);
+ }
+
+ public String parseString () {
+ return parseString(false);
+ }
+
+ public String parseStringOrAtom () {
+ return parseString(true);
+ }
+
+ public String parseString (boolean orAtom) {
+ skipWhite();
+ char c = nextChar();
+ if (c != '\"') {
+ if (c == '{') {
+ int cnt = parseInteger();
+ c = nextChar();
+ if (c != '}')
+ return null;
+ int start = pos + 2;
+ int end = start + cnt;
+ String s = str.substring(start, end);
+ pos = end;
+ return s;
+ } else if (orAtom) {
+ backChar();
+ return parseAtom();
+ } else if (c == 'n' || c == 'N') {
+ parseAtom();
+ return null;
+ } else
+ return null;
+ }
+ int start = pos;
+ boolean quote = false;
+ while (true) {
+ c = nextChar();
+ if (c == 0)
+ return null;
+ else if (quote)
+ quote = false;
+ else if (c == '\\')
+ quote = true;
+ else if (c == '\"')
+ break;
+ }
+ return str.substring(start, pos - 1);
+ }
+
+ public void backChar () {
+ if (pos > 0)
+ pos--;
+ }
+
+ public String parseListOrNil () {
+ String list = parseList();
+ if (list == null) {
+ parseAtom();
+ list = "";
+ }
+ return list;
+ }
+
+ public String parseList () {
+ skipWhite();
+ if (nextChar() != '(') {
+ backChar();
+ return null;
+ }
+ int start = pos;
+ int level = 0;
+ boolean quote = false;
+ boolean string = false;
+ while (true) {
+ char c = nextChar();
+ if (c == 0)
+ return null;
+ else if (quote)
+ quote = false;
+ else if (c == '\\' && string)
+ quote = true;
+ else if (c == '\"')
+ string = !string;
+ else if (c == '(' && !string)
+ level++;
+ else if (c == ')' && !string) {
+ if (level-- == 0)
+ break;
+ }
+ }
+ return str.substring(start, pos - 1);
+ }
+
+ public Integer parseInteger () {
+ skipWhite();
+ int start = pos;
+ while (pos < len) {
+ char c = str.charAt(pos);
+ if (c >= '0' && c <= '9')
+ pos++;
+ else
+ break;
+ }
+ if (pos > start) {
+ try {
+ Integer i = Integer.parseInt(str.substring(start, pos));
+ return i;
+ } catch (NumberFormatException e) {
+ return -1;
+ }
+ } else
+ return -1;
+ }
+
+ public int[] gatherInts () {
+ int[] list = new int[128];
+ int size = 128;
+ int offs = 0;
+ while (true) {
+ // TODO Slow; handle this inline rather than calling the method
+ Integer i = parseInteger();
+ if (i >= 0) {
+ if (offs == size) {
+ // Double the size of the array as necessary
+ size <<= 1;
+ int[] tmp = new int[size];
+ System.arraycopy(list, 0, tmp, 0, offs);
+ list = tmp;
+ }
+ list[offs++] = i;
+ }
+ else
+ break;
+ }
+ int[] res = new int[offs];
+ System.arraycopy(list, 0, res, 0, offs);
+ return res;
+ }
+}
diff --git a/imap2/src/com/android/imap2/QuotedPrintable.java b/imap2/src/com/android/imap2/QuotedPrintable.java
new file mode 100644
index 000000000..6171d9224
--- /dev/null
+++ b/imap2/src/com/android/imap2/QuotedPrintable.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.imap2;
+
+public class QuotedPrintable {
+ static public String toString (String str) {
+ int len = str.length();
+ // Make sure we don't get an index out of bounds error with the = character
+ int max = len - 2;
+ StringBuilder sb = new StringBuilder(len);
+ try {
+ for (int i = 0; i < len; i++) {
+ char c = str.charAt(i);
+ if (c == '=') {
+ if (i < max) {
+ char n = str.charAt(++i);
+ if (n == '\r') {
+ n = str.charAt(++i);
+ if (n == '\n')
+ continue;
+ else
+ System.err.println("Not valid QP");
+ } else {
+ // Must be less than 0x80, right?
+ int a;
+ if (n >= '0' && n <= '9')
+ a = (n - '0') << 4;
+ else
+ a = (10 + (n - 'A')) << 4;
+
+ n = str.charAt(++i);
+ if (n >= '0' && n <= '9')
+ c = (char) (a + (n - '0'));
+ else
+ c = (char) (a + 10 + (n - 'A'));
+ }
+ } if (i + 1 == len)
+ continue;
+ }
+
+ sb.append(c);
+ }
+ } catch (IndexOutOfBoundsException e) {
+ }
+ String ret = sb.toString();
+ return ret;
+ }
+
+ static public String encode (String str) {
+ int len = str.length();
+ StringBuffer sb = new StringBuffer(len + len>>2);
+ int i = 0;
+ while (i < len) {
+ char c = str.charAt(i++);
+ if (c < 0x80) {
+ sb.append(c);
+ } else {
+ sb.append('&');
+ sb.append('#');
+ sb.append((int)c);
+ sb.append(';');
+ }
+ }
+ return sb.toString();
+ }
+
+ static public int decode (byte[] bytes, int len) {
+ // Make sure we don't get an index out of bounds error with the = character
+ int max = len - 2;
+ int pos = 0;
+ try {
+ for (int i = 0; i < len; i++) {
+ char c = (char)bytes[i];
+ if (c == '=') {
+ if (i < max) {
+ char n = (char)bytes[++i];
+ if (n == '\r') {
+ n = (char)bytes[++i];
+ if (n == '\n')
+ continue;
+ else
+ System.err.println("Not valid QP");
+ } else {
+ // Must be less than 0x80, right?
+ int a;
+ if (n >= '0' && n <= '9')
+ a = (n - '0') << 4;
+ else
+ a = (10 + (n - 'A')) << 4;
+
+ n = (char)bytes[++i];
+ if (n >= '0' && n <= '9')
+ c = (char) (a + (n - '0'));
+ else
+ c = (char) (a + 10 + (n - 'A'));
+ }
+ } if (i + 1 > len)
+ continue;
+ }
+
+ bytes[pos++] = (byte)c;
+ }
+ } catch (IndexOutOfBoundsException e) {
+ }
+ return pos;
+ }
+}
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 237ff1164..7ad3bcb2d 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1300,5 +1300,13 @@ as %s.