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      */
newInstance(Account account, Context context)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      */
ImapStore(Context context, Account account)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 
getUseOAuth()125     boolean getUseOAuth() {
126         return mUseOAuth;
127     }
128 
getUsername()129     String getUsername() {
130         return mUsername;
131     }
132 
getPassword()133     String getPassword() {
134         return mPassword;
135     }
136 
canSyncFolderType(final int type)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
getConnectionPoolForTest()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
setTransportForTest(MailTransport testTransport)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      */
getImapId(Context context, String userName, String host, String capabilities)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
makeCommonImapId(String packageName, String version, String codeName, String model, String id, String vendor, String networkOperator)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
getFolder(String name)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
createHierarchy(HashMap<String, ImapFolder> mailboxes)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      */
addMailbox(Context context, long accountId, String mailboxPath, char delimiter, boolean selectable, Mailbox mailbox)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      */
saveMailboxList(Context context, HashMap<String, ImapFolder> folderMap)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
updateFolders()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
checkSettings()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      */
isUserPrefixSet()511     boolean isUserPrefixSet() {
512         return TextUtils.isEmpty(mPathSeparator) && !TextUtils.isEmpty(mPathPrefix);
513     }
514 
515     /** Sets the path separator */
setPathSeparator(String pathSeparator)516     void setPathSeparator(String pathSeparator) {
517         mPathSeparator = pathSeparator;
518     }
519 
520     /** Sets the prefix */
setPathPrefix(String pathPrefix)521     void setPathPrefix(String pathPrefix) {
522         mPathPrefix = pathPrefix;
523     }
524 
525     /** Gets the context for this store */
getContext()526     Context getContext() {
527         return mContext;
528     }
529 
530     /** Returns a clone of the transport associated with this store. */
cloneTransport()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      */
ensurePrefixIsValid()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      */
getConnection()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      */
poolConnection(ImapConnection connection)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      */
encodeFolderName(String name, String prefix)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      */
decodeFolderName(String name, String prefix)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      */
joinMessageUids(Message[] messages)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 {
ImapMessage(String uid, ImapFolder folder)636         ImapMessage(String uid, ImapFolder folder) {
637             mUid = uid;
638             mFolder = folder;
639         }
640 
setSize(int size)641         public void setSize(int size) {
642             mSize = size;
643         }
644 
645         @Override
parse(InputStream in)646         public void parse(InputStream in) throws IOException, MessagingException {
647             super.parse(in);
648         }
649 
setFlagInternal(Flag flag, boolean set)650         public void setFlagInternal(Flag flag, boolean set) throws MessagingException {
651             super.setFlag(flag, set);
652         }
653 
654         @Override
setFlag(Flag flag, boolean set)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 
ImapException(String message, String status, String alertText, String responseCode)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 
getStatus()676         public String getStatus() {
677             return mStatus;
678         }
679 
getAlertText()680         public String getAlertText() {
681             return mAlertText;
682         }
683 
getResponseCode()684         public String getResponseCode() {
685             return mResponseCode;
686         }
687     }
688 
closeConnections()689     public void closeConnections() {
690         ImapConnection connection;
691         while ((connection = mConnectionPool.poll()) != null) {
692             connection.close();
693         }
694     }
695 }
696