Home | History | Annotate | Download | only in store
      1 /*
      2  * Copyright (C) 2008 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.email.mail.store;
     18 
     19 import android.content.Context;
     20 import android.os.Build;
     21 import android.os.Bundle;
     22 import android.telephony.TelephonyManager;
     23 import android.text.TextUtils;
     24 import android.util.Base64;
     25 
     26 import com.android.email.LegacyConversions;
     27 import com.android.email.Preferences;
     28 import com.android.email.mail.Store;
     29 import com.android.email.mail.store.imap.ImapConstants;
     30 import com.android.email.mail.store.imap.ImapResponse;
     31 import com.android.email.mail.store.imap.ImapString;
     32 import com.android.email.mail.transport.MailTransport;
     33 import com.android.emailcommon.Logging;
     34 import com.android.emailcommon.VendorPolicyLoader;
     35 import com.android.emailcommon.internet.MimeMessage;
     36 import com.android.emailcommon.mail.AuthenticationFailedException;
     37 import com.android.emailcommon.mail.Flag;
     38 import com.android.emailcommon.mail.Folder;
     39 import com.android.emailcommon.mail.Message;
     40 import com.android.emailcommon.mail.MessagingException;
     41 import com.android.emailcommon.provider.Account;
     42 import com.android.emailcommon.provider.Credential;
     43 import com.android.emailcommon.provider.EmailContent;
     44 import com.android.emailcommon.provider.HostAuth;
     45 import com.android.emailcommon.provider.Mailbox;
     46 import com.android.emailcommon.service.EmailServiceProxy;
     47 import com.android.emailcommon.utility.Utility;
     48 import com.android.mail.utils.LogUtils;
     49 import com.beetstra.jutf7.CharsetProvider;
     50 import com.google.common.annotations.VisibleForTesting;
     51 
     52 import java.io.IOException;
     53 import java.io.InputStream;
     54 import java.nio.ByteBuffer;
     55 import java.nio.charset.Charset;
     56 import java.security.MessageDigest;
     57 import java.security.NoSuchAlgorithmException;
     58 import java.util.Collection;
     59 import java.util.HashMap;
     60 import java.util.List;
     61 import java.util.Set;
     62 import java.util.concurrent.ConcurrentLinkedQueue;
     63 import java.util.regex.Pattern;
     64 
     65 
     66 /**
     67  * <pre>
     68  * TODO Need to start keeping track of UIDVALIDITY
     69  * TODO Need a default response handler for things like folder updates
     70  * TODO In fetch(), if we need a ImapMessage and were given
     71  *      something else we can try to do a pre-fetch first.
     72  * TODO Collect ALERT messages and show them to users.
     73  *
     74  * ftp://ftp.isi.edu/in-notes/rfc2683.txt When a client asks for
     75  * certain information in a FETCH command, the server may return the requested
     76  * information in any order, not necessarily in the order that it was requested.
     77  * Further, the server may return the information in separate FETCH responses
     78  * and may also return information that was not explicitly requested (to reflect
     79  * to the client changes in the state of the subject message).
     80  * </pre>
     81  */
     82 public class ImapStore extends Store {
     83     /** Charset used for converting folder names to and from UTF-7 as defined by RFC 3501. */
     84     private static final Charset MODIFIED_UTF_7_CHARSET =
     85             new CharsetProvider().charsetForName("X-RFC-3501");
     86 
     87     @VisibleForTesting static String sImapId = null;
     88     @VisibleForTesting String mPathPrefix;
     89     @VisibleForTesting String mPathSeparator;
     90 
     91     private boolean mUseOAuth;
     92 
     93     private final ConcurrentLinkedQueue<ImapConnection> mConnectionPool =
     94             new ConcurrentLinkedQueue<ImapConnection>();
     95 
     96     /**
     97      * Static named constructor.
     98      */
     99     public static Store newInstance(Account account, Context context) throws MessagingException {
    100         return new ImapStore(context, account);
    101     }
    102 
    103     /**
    104      * Creates a new store for the given account. Always use
    105      * {@link #newInstance(Account, Context)} to create an IMAP store.
    106      */
    107     private ImapStore(Context context, Account account) throws MessagingException {
    108         mContext = context;
    109         mAccount = account;
    110 
    111         HostAuth recvAuth = account.getOrCreateHostAuthRecv(context);
    112         if (recvAuth == null) {
    113             throw new MessagingException("No HostAuth in ImapStore?");
    114         }
    115         mTransport = new MailTransport(context, "IMAP", recvAuth);
    116 
    117         String[] userInfo = recvAuth.getLogin();
    118         mUsername = userInfo[0];
    119         mPassword = userInfo[1];
    120         final Credential cred = recvAuth.getCredential(context);
    121         mUseOAuth = (cred != null);
    122         mPathPrefix = recvAuth.mDomain;
    123     }
    124 
    125     boolean getUseOAuth() {
    126         return mUseOAuth;
    127     }
    128 
    129     String getUsername() {
    130         return mUsername;
    131     }
    132 
    133     String getPassword() {
    134         return mPassword;
    135     }
    136 
    137     public boolean canSyncFolderType(final int type) {
    138         switch (type) {
    139             case Mailbox.TYPE_INBOX:
    140             case Mailbox.TYPE_MAIL:
    141             case Mailbox.TYPE_SENT:
    142             case Mailbox.TYPE_TRASH:
    143             case Mailbox.TYPE_JUNK:
    144                 return true;
    145             case Mailbox.TYPE_NONE:
    146             case Mailbox.TYPE_PARENT:
    147             case Mailbox.TYPE_DRAFTS:
    148             case Mailbox.TYPE_OUTBOX:
    149             case Mailbox.TYPE_SEARCH:
    150             case Mailbox.TYPE_STARRED:
    151             case Mailbox.TYPE_UNREAD:
    152             default:
    153                 return false;
    154         }
    155     }
    156 
    157     @VisibleForTesting
    158     Collection<ImapConnection> getConnectionPoolForTest() {
    159         return mConnectionPool;
    160     }
    161 
    162     /**
    163      * For testing only.  Injects a different root transport (it will be copied using
    164      * newInstanceWithConfiguration() each time IMAP sets up a new channel).  The transport
    165      * should already be set up and ready to use.  Do not use for real code.
    166      * @param testTransport The Transport to inject and use for all future communication.
    167      */
    168     @VisibleForTesting
    169     void setTransportForTest(MailTransport testTransport) {
    170         mTransport = testTransport;
    171     }
    172 
    173     /**
    174      * Return, or create and return, an string suitable for use in an IMAP ID message.
    175      * This is constructed similarly to the way the browser sets up its user-agent strings.
    176      * See RFC 2971 for more details.  The output of this command will be a series of key-value
    177      * pairs delimited by spaces (there is no point in returning a structured result because
    178      * this will be sent as-is to the IMAP server).  No tokens, parenthesis or "ID" are included,
    179      * because some connections may append additional values.
    180      *
    181      * The following IMAP ID keys may be included:
    182      *   name                   Android package name of the program
    183      *   os                     "android"
    184      *   os-version             "version; model; build-id"
    185      *   vendor                 Vendor of the client/server
    186      *   x-android-device-model Model (only revealed if release build)
    187      *   x-android-net-operator Mobile network operator (if known)
    188      *   AGUID                  A device+account UID
    189      *
    190      * In addition, a vendor policy .apk can append key/value pairs.
    191      *
    192      * @param userName the username of the account
    193      * @param host the host (server) of the account
    194      * @param capabilities a list of the capabilities from the server
    195      * @return a String for use in an IMAP ID message.
    196      */
    197     public static String getImapId(Context context, String userName, String host,
    198             String capabilities) {
    199         // The first section is global to all IMAP connections, and generates the fixed
    200         // values in any IMAP ID message
    201         synchronized (ImapStore.class) {
    202             if (sImapId == null) {
    203                 TelephonyManager tm =
    204                         (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
    205                 String networkOperator = tm.getNetworkOperatorName();
    206                 if (networkOperator == null) networkOperator = "";
    207 
    208                 sImapId = makeCommonImapId(context.getPackageName(), Build.VERSION.RELEASE,
    209                         Build.VERSION.CODENAME, Build.MODEL, Build.ID, Build.MANUFACTURER,
    210                         networkOperator);
    211             }
    212         }
    213 
    214         // This section is per Store, and adds in a dynamic elements like UID's.
    215         // We don't cache the result of this work, because the caller does anyway.
    216         StringBuilder id = new StringBuilder(sImapId);
    217 
    218         // Optionally add any vendor-supplied id keys
    219         String vendorId = VendorPolicyLoader.getInstance(context).getImapIdValues(userName, host,
    220                 capabilities);
    221         if (vendorId != null) {
    222             id.append(' ');
    223             id.append(vendorId);
    224         }
    225 
    226         // Generate a UID that mixes a "stable" device UID with the email address
    227         try {
    228             String devUID = Preferences.getPreferences(context).getDeviceUID();
    229             MessageDigest messageDigest;
    230             messageDigest = MessageDigest.getInstance("SHA-1");
    231             messageDigest.update(userName.getBytes());
    232             messageDigest.update(devUID.getBytes());
    233             byte[] uid = messageDigest.digest();
    234             String hexUid = Base64.encodeToString(uid, Base64.NO_WRAP);
    235             id.append(" \"AGUID\" \"");
    236             id.append(hexUid);
    237             id.append('\"');
    238         } catch (NoSuchAlgorithmException e) {
    239             LogUtils.d(Logging.LOG_TAG, "couldn't obtain SHA-1 hash for device UID");
    240         }
    241         return id.toString();
    242     }
    243 
    244     /**
    245      * Helper function that actually builds the static part of the IMAP ID string.  This is
    246      * separated from getImapId for testability.  There is no escaping or encoding in IMAP ID so
    247      * any rogue chars must be filtered here.
    248      *
    249      * @param packageName context.getPackageName()
    250      * @param version Build.VERSION.RELEASE
    251      * @param codeName Build.VERSION.CODENAME
    252      * @param model Build.MODEL
    253      * @param id Build.ID
    254      * @param vendor Build.MANUFACTURER
    255      * @param networkOperator TelephonyManager.getNetworkOperatorName()
    256      * @return the static (never changes) portion of the IMAP ID
    257      */
    258     @VisibleForTesting
    259     static String makeCommonImapId(String packageName, String version,
    260             String codeName, String model, String id, String vendor, String networkOperator) {
    261 
    262         // Before building up IMAP ID string, pre-filter the input strings for "legal" chars
    263         // This is using a fairly arbitrary char set intended to pass through most reasonable
    264         // version, model, and vendor strings: a-z A-Z 0-9 - _ + = ; : . , / <space>
    265         // The most important thing is *not* to pass parens, quotes, or CRLF, which would break
    266         // the format of the IMAP ID list.
    267         Pattern p = Pattern.compile("[^a-zA-Z0-9-_\\+=;:\\.,/ ]");
    268         packageName = p.matcher(packageName).replaceAll("");
    269         version = p.matcher(version).replaceAll("");
    270         codeName = p.matcher(codeName).replaceAll("");
    271         model = p.matcher(model).replaceAll("");
    272         id = p.matcher(id).replaceAll("");
    273         vendor = p.matcher(vendor).replaceAll("");
    274         networkOperator = p.matcher(networkOperator).replaceAll("");
    275 
    276         // "name" "com.android.email"
    277         StringBuilder sb = new StringBuilder("\"name\" \"");
    278         sb.append(packageName);
    279         sb.append("\"");
    280 
    281         // "os" "android"
    282         sb.append(" \"os\" \"android\"");
    283 
    284         // "os-version" "version; build-id"
    285         sb.append(" \"os-version\" \"");
    286         if (version.length() > 0) {
    287             sb.append(version);
    288         } else {
    289             // default to "1.0"
    290             sb.append("1.0");
    291         }
    292         // add the build ID or build #
    293         if (id.length() > 0) {
    294             sb.append("; ");
    295             sb.append(id);
    296         }
    297         sb.append("\"");
    298 
    299         // "vendor" "the vendor"
    300         if (vendor.length() > 0) {
    301             sb.append(" \"vendor\" \"");
    302             sb.append(vendor);
    303             sb.append("\"");
    304         }
    305 
    306         // "x-android-device-model" the device model (on release builds only)
    307         if ("REL".equals(codeName)) {
    308             if (model.length() > 0) {
    309                 sb.append(" \"x-android-device-model\" \"");
    310                 sb.append(model);
    311                 sb.append("\"");
    312             }
    313         }
    314 
    315         // "x-android-mobile-net-operator" "name of network operator"
    316         if (networkOperator.length() > 0) {
    317             sb.append(" \"x-android-mobile-net-operator\" \"");
    318             sb.append(networkOperator);
    319             sb.append("\"");
    320         }
    321 
    322         return sb.toString();
    323     }
    324 
    325 
    326     @Override
    327     public Folder getFolder(String name) {
    328         return new ImapFolder(this, name);
    329     }
    330 
    331     /**
    332      * Creates a mailbox hierarchy out of the flat data provided by the server.
    333      */
    334     @VisibleForTesting
    335     static void createHierarchy(HashMap<String, ImapFolder> mailboxes) {
    336         Set<String> pathnames = mailboxes.keySet();
    337         for (String path : pathnames) {
    338             final ImapFolder folder = mailboxes.get(path);
    339             final Mailbox mailbox = folder.mMailbox;
    340             int delimiterIdx = mailbox.mServerId.lastIndexOf(mailbox.mDelimiter);
    341             long parentKey = Mailbox.NO_MAILBOX;
    342             String parentPath = null;
    343             if (delimiterIdx != -1) {
    344                 parentPath = path.substring(0, delimiterIdx);
    345                 if (ImapConstants.INBOX.equalsIgnoreCase(parentPath)) {
    346                     // The Inbox is added as a special case, and always in all caps. In reality,
    347                     // it might not be in all caps, this folder's parent path might have mixed case.
    348                     parentPath = ImapConstants.INBOX;
    349                 }
    350                 final ImapFolder parentFolder = mailboxes.get(parentPath);
    351                 final Mailbox parentMailbox = (parentFolder == null) ? null : parentFolder.mMailbox;
    352                 if (parentMailbox != null) {
    353                     parentKey = parentMailbox.mId;
    354                     parentMailbox.mFlags
    355                             |= (Mailbox.FLAG_HAS_CHILDREN | Mailbox.FLAG_CHILDREN_VISIBLE);
    356                 }
    357             }
    358             mailbox.mParentKey = parentKey;
    359             mailbox.mParentServerId = parentPath;
    360         }
    361     }
    362 
    363     /**
    364      * Creates a {@link Folder} and associated {@link Mailbox}. If the folder does not already
    365      * exist in the local database, a new row will immediately be created in the mailbox table.
    366      * Otherwise, the existing row will be used. Any changes to existing rows, will not be stored
    367      * to the database immediately.
    368      * @param accountId The ID of the account the mailbox is to be associated with
    369      * @param mailboxPath The path of the mailbox to add
    370      * @param delimiter A path delimiter. May be {@code null} if there is no delimiter.
    371      * @param selectable If {@code true}, the mailbox can be selected and used to store messages.
    372      * @param mailbox If not null, mailbox is used instead of querying for the Mailbox.
    373      */
    374     private ImapFolder addMailbox(Context context, long accountId, String mailboxPath,
    375             char delimiter, boolean selectable, Mailbox mailbox) {
    376         // TODO: pass in the mailbox type, or do a proper lookup here
    377         final int mailboxType;
    378         if (mailbox == null) {
    379             mailboxType = LegacyConversions.inferMailboxTypeFromName(context, mailboxPath);
    380             mailbox = Mailbox.getMailboxForPath(context, accountId, mailboxPath);
    381         } else {
    382             mailboxType = mailbox.mType;
    383         }
    384         final ImapFolder folder = (ImapFolder) getFolder(mailboxPath);
    385         if (mailbox.isSaved()) {
    386             // existing mailbox
    387             // mailbox retrieved from database; save hash _before_ updating fields
    388             folder.mHash = mailbox.getHashes();
    389         }
    390         updateMailbox(mailbox, accountId, mailboxPath, delimiter, selectable, mailboxType);
    391         if (folder.mHash == null) {
    392             // new mailbox
    393             // save hash after updating. allows tracking changes if the mailbox is saved
    394             // outside of #saveMailboxList()
    395             folder.mHash = mailbox.getHashes();
    396             // We must save this here to make sure we have a valid ID for later
    397 
    398             // This is a newly created folder from the server. By definition, if it came from
    399             // the server, it can be synched. We need to set the uiSyncStatus so that the UI
    400             // will not try to display the empty state until the sync completes.
    401             mailbox.mUiSyncStatus = EmailContent.SYNC_STATUS_INITIAL_SYNC_NEEDED;
    402             mailbox.save(mContext);
    403         }
    404         folder.mMailbox = mailbox;
    405         return folder;
    406     }
    407 
    408     /**
    409      * Persists the folders in the given list.
    410      */
    411     private static void saveMailboxList(Context context, HashMap<String, ImapFolder> folderMap) {
    412         for (ImapFolder imapFolder : folderMap.values()) {
    413             imapFolder.save(context);
    414         }
    415     }
    416 
    417     @Override
    418     public Folder[] updateFolders() throws MessagingException {
    419         // TODO: There is nothing that ever closes this connection. Trouble is, it's not exactly
    420         // clear when we should close it, we'd like to keep it open until we're really done
    421         // using it.
    422         ImapConnection connection = getConnection();
    423         try {
    424             final HashMap<String, ImapFolder> mailboxes = new HashMap<String, ImapFolder>();
    425             // Establish a connection to the IMAP server; if necessary
    426             // This ensures a valid prefix if the prefix is automatically set by the server
    427             connection.executeSimpleCommand(ImapConstants.NOOP);
    428             String imapCommand = ImapConstants.LIST + " \"\" \"*\"";
    429             if (mPathPrefix != null) {
    430                 imapCommand = ImapConstants.LIST + " \"\" \"" + mPathPrefix + "*\"";
    431             }
    432             List<ImapResponse> responses = connection.executeSimpleCommand(imapCommand);
    433             for (ImapResponse response : responses) {
    434                 // S: * LIST (\Noselect) "/" ~/Mail/foo
    435                 if (response.isDataResponse(0, ImapConstants.LIST)) {
    436                     // Get folder name.
    437                     ImapString encodedFolder = response.getStringOrEmpty(3);
    438                     if (encodedFolder.isEmpty()) continue;
    439 
    440                     String folderName = decodeFolderName(encodedFolder.getString(), mPathPrefix);
    441 
    442                     if (ImapConstants.INBOX.equalsIgnoreCase(folderName)) continue;
    443 
    444                     // Parse attributes.
    445                     boolean selectable =
    446                         !response.getListOrEmpty(1).contains(ImapConstants.FLAG_NO_SELECT);
    447                     String delimiter = response.getStringOrEmpty(2).getString();
    448                     char delimiterChar = '\0';
    449                     if (!TextUtils.isEmpty(delimiter)) {
    450                         delimiterChar = delimiter.charAt(0);
    451                     }
    452                     ImapFolder folder = addMailbox(
    453                             mContext, mAccount.mId, folderName, delimiterChar, selectable, null);
    454                     mailboxes.put(folderName, folder);
    455                 }
    456             }
    457 
    458             // In order to properly map INBOX -> Inbox, handle it as a special case.
    459             final Mailbox inbox =
    460                     Mailbox.restoreMailboxOfType(mContext, mAccount.mId, Mailbox.TYPE_INBOX);
    461             final ImapFolder newFolder = addMailbox(
    462                     mContext, mAccount.mId, inbox.mServerId, '\0', true /*selectable*/, inbox);
    463             mailboxes.put(ImapConstants.INBOX, newFolder);
    464 
    465             createHierarchy(mailboxes);
    466             saveMailboxList(mContext, mailboxes);
    467             return mailboxes.values().toArray(new Folder[mailboxes.size()]);
    468         } catch (IOException ioe) {
    469             connection.close();
    470             throw new MessagingException("Unable to get folder list", ioe);
    471         } catch (AuthenticationFailedException afe) {
    472             // We do NOT want this connection pooled, or we will continue to send NOOP and SELECT
    473             // commands to the server
    474             connection.destroyResponses();
    475             connection = null;
    476             throw afe;
    477         } finally {
    478             if (connection != null) {
    479                 // We keep our connection out of the pool as long as we are using it, then
    480                 // put it back into the pool so it can be reused.
    481                 poolConnection(connection);
    482             }
    483         }
    484     }
    485 
    486     @Override
    487     public Bundle checkSettings() throws MessagingException {
    488         int result = MessagingException.NO_ERROR;
    489         Bundle bundle = new Bundle();
    490         // TODO: why doesn't this use getConnection()? I guess this is only done during setup,
    491         // so there's need to look for a pooled connection?
    492         // But then why doesn't it use poolConnection() after it's done?
    493         ImapConnection connection = new ImapConnection(this);
    494         try {
    495             connection.open();
    496             connection.close();
    497         } catch (IOException ioe) {
    498             bundle.putString(EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE, ioe.getMessage());
    499             result = MessagingException.IOERROR;
    500         } finally {
    501             connection.destroyResponses();
    502         }
    503         bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, result);
    504         return bundle;
    505     }
    506 
    507     /**
    508      * Returns whether or not the prefix has been set by the user. This can be determined by
    509      * the fact that the prefix is set, but, the path separator is not set.
    510      */
    511     boolean isUserPrefixSet() {
    512         return TextUtils.isEmpty(mPathSeparator) && !TextUtils.isEmpty(mPathPrefix);
    513     }
    514 
    515     /** Sets the path separator */
    516     void setPathSeparator(String pathSeparator) {
    517         mPathSeparator = pathSeparator;
    518     }
    519 
    520     /** Sets the prefix */
    521     void setPathPrefix(String pathPrefix) {
    522         mPathPrefix = pathPrefix;
    523     }
    524 
    525     /** Gets the context for this store */
    526     Context getContext() {
    527         return mContext;
    528     }
    529 
    530     /** Returns a clone of the transport associated with this store. */
    531     MailTransport cloneTransport() {
    532         return mTransport.clone();
    533     }
    534 
    535     /**
    536      * Fixes the path prefix, if necessary. The path prefix must always end with the
    537      * path separator.
    538      */
    539     void ensurePrefixIsValid() {
    540         // Make sure the path prefix ends with the path separator
    541         if (!TextUtils.isEmpty(mPathPrefix) && !TextUtils.isEmpty(mPathSeparator)) {
    542             if (!mPathPrefix.endsWith(mPathSeparator)) {
    543                 mPathPrefix = mPathPrefix + mPathSeparator;
    544             }
    545         }
    546     }
    547 
    548     /**
    549      * Gets a connection if one is available from the pool, or creates a new one if not.
    550      */
    551     ImapConnection getConnection() {
    552         // TODO Why would we ever have (or need to have) more than one active connection?
    553         // TODO We set new username/password each time, but we don't actually close the transport
    554         // when we do this. So if that information has changed, this connection will fail.
    555         ImapConnection connection;
    556         while ((connection = mConnectionPool.poll()) != null) {
    557             try {
    558                 connection.setStore(this);
    559                 connection.executeSimpleCommand(ImapConstants.NOOP);
    560                 break;
    561             } catch (MessagingException e) {
    562                 // Fall through
    563             } catch (IOException e) {
    564                 // Fall through
    565             }
    566             connection.close();
    567         }
    568 
    569         if (connection == null) {
    570             connection = new ImapConnection(this);
    571         }
    572         return connection;
    573     }
    574 
    575     /**
    576      * Save a {@link ImapConnection} in the pool for reuse. Any responses associated with the
    577      * connection are destroyed before adding the connection to the pool.
    578      */
    579     void poolConnection(ImapConnection connection) {
    580         if (connection != null) {
    581             connection.destroyResponses();
    582             mConnectionPool.add(connection);
    583         }
    584     }
    585 
    586     /**
    587      * Prepends the folder name with the given prefix and UTF-7 encodes it.
    588      */
    589     static String encodeFolderName(String name, String prefix) {
    590         // do NOT add the prefix to the special name "INBOX"
    591         if (ImapConstants.INBOX.equalsIgnoreCase(name)) return name;
    592 
    593         // Prepend prefix
    594         if (prefix != null) {
    595             name = prefix + name;
    596         }
    597 
    598         // TODO bypass the conversion if name doesn't have special char.
    599         ByteBuffer bb = MODIFIED_UTF_7_CHARSET.encode(name);
    600         byte[] b = new byte[bb.limit()];
    601         bb.get(b);
    602 
    603         return Utility.fromAscii(b);
    604     }
    605 
    606     /**
    607      * UTF-7 decodes the folder name and removes the given path prefix.
    608      */
    609     static String decodeFolderName(String name, String prefix) {
    610         // TODO bypass the conversion if name doesn't have special char.
    611         String folder;
    612         folder = MODIFIED_UTF_7_CHARSET.decode(ByteBuffer.wrap(Utility.toAscii(name))).toString();
    613         if ((prefix != null) && folder.startsWith(prefix)) {
    614             folder = folder.substring(prefix.length());
    615         }
    616         return folder;
    617     }
    618 
    619     /**
    620      * Returns UIDs of Messages joined with "," as the separator.
    621      */
    622     static String joinMessageUids(Message[] messages) {
    623         StringBuilder sb = new StringBuilder();
    624         boolean notFirst = false;
    625         for (Message m : messages) {
    626             if (notFirst) {
    627                 sb.append(',');
    628             }
    629             sb.append(m.getUid());
    630             notFirst = true;
    631         }
    632         return sb.toString();
    633     }
    634 
    635     static class ImapMessage extends MimeMessage {
    636         ImapMessage(String uid, ImapFolder folder) {
    637             mUid = uid;
    638             mFolder = folder;
    639         }
    640 
    641         public void setSize(int size) {
    642             mSize = size;
    643         }
    644 
    645         @Override
    646         public void parse(InputStream in) throws IOException, MessagingException {
    647             super.parse(in);
    648         }
    649 
    650         public void setFlagInternal(Flag flag, boolean set) throws MessagingException {
    651             super.setFlag(flag, set);
    652         }
    653 
    654         @Override
    655         public void setFlag(Flag flag, boolean set) throws MessagingException {
    656             super.setFlag(flag, set);
    657             mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set);
    658         }
    659     }
    660 
    661     static class ImapException extends MessagingException {
    662         private static final long serialVersionUID = 1L;
    663 
    664         private final String mStatus;
    665         private final String mAlertText;
    666         private final String mResponseCode;
    667 
    668         public ImapException(String message, String status, String alertText,
    669                 String responseCode) {
    670             super(message);
    671             mStatus = status;
    672             mAlertText = alertText;
    673             mResponseCode = responseCode;
    674         }
    675 
    676         public String getStatus() {
    677             return mStatus;
    678         }
    679 
    680         public String getAlertText() {
    681             return mAlertText;
    682         }
    683 
    684         public String getResponseCode() {
    685             return mResponseCode;
    686         }
    687     }
    688 
    689     public void closeConnections() {
    690         ImapConnection connection;
    691         while ((connection = mConnectionPool.poll()) != null) {
    692             connection.close();
    693         }
    694     }
    695 }
    696