• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 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.text.TextUtils;
21 import android.text.format.DateUtils;
22 import android.util.Base64DataException;
23 
24 import com.android.email.mail.store.ImapStore.ImapException;
25 import com.android.email.mail.store.ImapStore.ImapMessage;
26 import com.android.email.mail.store.imap.ImapConstants;
27 import com.android.email.mail.store.imap.ImapElement;
28 import com.android.email.mail.store.imap.ImapList;
29 import com.android.email.mail.store.imap.ImapResponse;
30 import com.android.email.mail.store.imap.ImapString;
31 import com.android.email.mail.store.imap.ImapUtility;
32 import com.android.email2.ui.MailActivityEmail;
33 import com.android.emailcommon.Logging;
34 import com.android.emailcommon.internet.BinaryTempFileBody;
35 import com.android.emailcommon.internet.MimeBodyPart;
36 import com.android.emailcommon.internet.MimeHeader;
37 import com.android.emailcommon.internet.MimeMultipart;
38 import com.android.emailcommon.internet.MimeUtility;
39 import com.android.emailcommon.mail.AuthenticationFailedException;
40 import com.android.emailcommon.mail.Body;
41 import com.android.emailcommon.mail.FetchProfile;
42 import com.android.emailcommon.mail.Flag;
43 import com.android.emailcommon.mail.Folder;
44 import com.android.emailcommon.mail.Message;
45 import com.android.emailcommon.mail.MessagingException;
46 import com.android.emailcommon.mail.Part;
47 import com.android.emailcommon.provider.Mailbox;
48 import com.android.emailcommon.service.SearchParams;
49 import com.android.emailcommon.utility.CountingOutputStream;
50 import com.android.emailcommon.utility.EOLConvertingOutputStream;
51 import com.android.emailcommon.utility.Utility;
52 import com.android.mail.utils.LogUtils;
53 import com.google.common.annotations.VisibleForTesting;
54 
55 import java.io.IOException;
56 import java.io.InputStream;
57 import java.io.OutputStream;
58 import java.text.SimpleDateFormat;
59 import java.util.ArrayList;
60 import java.util.Arrays;
61 import java.util.Date;
62 import java.util.Locale;
63 import java.util.HashMap;
64 import java.util.LinkedHashSet;
65 import java.util.List;
66 import java.util.TimeZone;
67 
68 class ImapFolder extends Folder {
69     private final static Flag[] PERMANENT_FLAGS =
70         { Flag.DELETED, Flag.SEEN, Flag.FLAGGED, Flag.ANSWERED };
71     private static final int COPY_BUFFER_SIZE = 16*1024;
72 
73     private final ImapStore mStore;
74     private final String mName;
75     private int mMessageCount = -1;
76     private ImapConnection mConnection;
77     private OpenMode mMode;
78     private boolean mExists;
79     /** The local mailbox associated with this remote folder */
80     Mailbox mMailbox;
81     /** A set of hashes that can be used to track dirtiness */
82     Object mHash[];
83 
ImapFolder(ImapStore store, String name)84     /*package*/ ImapFolder(ImapStore store, String name) {
85         mStore = store;
86         mName = name;
87     }
88 
destroyResponses()89     private void destroyResponses() {
90         if (mConnection != null) {
91             mConnection.destroyResponses();
92         }
93     }
94 
95     @Override
open(OpenMode mode)96     public void open(OpenMode mode)
97             throws MessagingException {
98         try {
99             if (isOpen()) {
100                 if (mMode == mode) {
101                     // Make sure the connection is valid.
102                     // If it's not we'll close it down and continue on to get a new one.
103                     try {
104                         mConnection.executeSimpleCommand(ImapConstants.NOOP);
105                         return;
106 
107                     } catch (IOException ioe) {
108                         ioExceptionHandler(mConnection, ioe);
109                     } finally {
110                         destroyResponses();
111                     }
112                 } else {
113                     // Return the connection to the pool, if exists.
114                     close(false);
115                 }
116             }
117             synchronized (this) {
118                 mConnection = mStore.getConnection();
119             }
120             // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft NonJunk
121             // $MDNSent)
122             // * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft
123             // NonJunk $MDNSent \*)] Flags permitted.
124             // * 23 EXISTS
125             // * 0 RECENT
126             // * OK [UIDVALIDITY 1125022061] UIDs valid
127             // * OK [UIDNEXT 57576] Predicted next UID
128             // 2 OK [READ-WRITE] Select completed.
129             try {
130                 doSelect();
131             } catch (IOException ioe) {
132                 throw ioExceptionHandler(mConnection, ioe);
133             } finally {
134                 destroyResponses();
135             }
136         } catch (AuthenticationFailedException e) {
137             // Don't cache this connection, so we're forced to try connecting/login again
138             mConnection = null;
139             close(false);
140             throw e;
141         } catch (MessagingException e) {
142             mExists = false;
143             close(false);
144             throw e;
145         }
146     }
147 
148     @Override
149     @VisibleForTesting
isOpen()150     public boolean isOpen() {
151         return mExists && mConnection != null;
152     }
153 
154     @Override
getMode()155     public OpenMode getMode() {
156         return mMode;
157     }
158 
159     @Override
close(boolean expunge)160     public void close(boolean expunge) {
161         // TODO implement expunge
162         mMessageCount = -1;
163         synchronized (this) {
164             mStore.poolConnection(mConnection);
165             mConnection = null;
166         }
167     }
168 
169     @Override
getName()170     public String getName() {
171         return mName;
172     }
173 
174     @Override
exists()175     public boolean exists() throws MessagingException {
176         if (mExists) {
177             return true;
178         }
179         /*
180          * This method needs to operate in the unselected mode as well as the selected mode
181          * so we must get the connection ourselves if it's not there. We are specifically
182          * not calling checkOpen() since we don't care if the folder is open.
183          */
184         ImapConnection connection = null;
185         synchronized(this) {
186             if (mConnection == null) {
187                 connection = mStore.getConnection();
188             } else {
189                 connection = mConnection;
190             }
191         }
192         try {
193             connection.executeSimpleCommand(String.format(Locale.US,
194                     ImapConstants.STATUS + " \"%s\" (" + ImapConstants.UIDVALIDITY + ")",
195                     ImapStore.encodeFolderName(mName, mStore.mPathPrefix)));
196             mExists = true;
197             return true;
198 
199         } catch (MessagingException me) {
200             // Treat IOERROR messaging exception as IOException
201             if (me.getExceptionType() == MessagingException.IOERROR) {
202                 throw me;
203             }
204             return false;
205 
206         } catch (IOException ioe) {
207             throw ioExceptionHandler(connection, ioe);
208 
209         } finally {
210             connection.destroyResponses();
211             if (mConnection == null) {
212                 mStore.poolConnection(connection);
213             }
214         }
215     }
216 
217     // IMAP supports folder creation
218     @Override
canCreate(FolderType type)219     public boolean canCreate(FolderType type) {
220         return true;
221     }
222 
223     @Override
create(FolderType type)224     public boolean create(FolderType type) throws MessagingException {
225         /*
226          * This method needs to operate in the unselected mode as well as the selected mode
227          * so we must get the connection ourselves if it's not there. We are specifically
228          * not calling checkOpen() since we don't care if the folder is open.
229          */
230         ImapConnection connection = null;
231         synchronized(this) {
232             if (mConnection == null) {
233                 connection = mStore.getConnection();
234             } else {
235                 connection = mConnection;
236             }
237         }
238         try {
239             connection.executeSimpleCommand(String.format(Locale.US,
240                     ImapConstants.CREATE + " \"%s\"",
241                     ImapStore.encodeFolderName(mName, mStore.mPathPrefix)));
242             return true;
243 
244         } catch (MessagingException me) {
245             return false;
246 
247         } catch (IOException ioe) {
248             throw ioExceptionHandler(connection, ioe);
249 
250         } finally {
251             connection.destroyResponses();
252             if (mConnection == null) {
253                 mStore.poolConnection(connection);
254             }
255         }
256     }
257 
258     @Override
copyMessages(Message[] messages, Folder folder, MessageUpdateCallbacks callbacks)259     public void copyMessages(Message[] messages, Folder folder,
260             MessageUpdateCallbacks callbacks) throws MessagingException {
261         checkOpen();
262         try {
263             List<ImapResponse> responseList = mConnection.executeSimpleCommand(
264                     String.format(Locale.US, ImapConstants.UID_COPY + " %s \"%s\"",
265                             ImapStore.joinMessageUids(messages),
266                             ImapStore.encodeFolderName(folder.getName(), mStore.mPathPrefix)));
267             // Build a message map for faster UID matching
268             HashMap<String, Message> messageMap = new HashMap<String, Message>();
269             boolean handledUidPlus = false;
270             for (Message m : messages) {
271                 messageMap.put(m.getUid(), m);
272             }
273             // Process response to get the new UIDs
274             for (ImapResponse response : responseList) {
275                 // All "BAD" responses are bad. Only "NO", tagged responses are bad.
276                 if (response.isBad() || (response.isNo() && response.isTagged())) {
277                     String responseText = response.getStatusResponseTextOrEmpty().getString();
278                     throw new MessagingException(responseText);
279                 }
280                 // Skip untagged responses; they're just status
281                 if (!response.isTagged()) {
282                     continue;
283                 }
284                 // No callback provided to report of UID changes; nothing more to do here
285                 // NOTE: We check this here to catch any server errors
286                 if (callbacks == null) {
287                     continue;
288                 }
289                 ImapList copyResponse = response.getListOrEmpty(1);
290                 String responseCode = copyResponse.getStringOrEmpty(0).getString();
291                 if (ImapConstants.COPYUID.equals(responseCode)) {
292                     handledUidPlus = true;
293                     String origIdSet = copyResponse.getStringOrEmpty(2).getString();
294                     String newIdSet = copyResponse.getStringOrEmpty(3).getString();
295                     String[] origIdArray = ImapUtility.getImapSequenceValues(origIdSet);
296                     String[] newIdArray = ImapUtility.getImapSequenceValues(newIdSet);
297                     // There has to be a 1:1 mapping between old and new IDs
298                     if (origIdArray.length != newIdArray.length) {
299                         throw new MessagingException("Set length mis-match; orig IDs \"" +
300                                 origIdSet + "\"  new IDs \"" + newIdSet + "\"");
301                     }
302                     for (int i = 0; i < origIdArray.length; i++) {
303                         final String id = origIdArray[i];
304                         final Message m = messageMap.get(id);
305                         if (m != null) {
306                             callbacks.onMessageUidChange(m, newIdArray[i]);
307                         }
308                     }
309                 }
310             }
311             // If the server doesn't support UIDPLUS, try a different way to get the new UID(s)
312             if (callbacks != null && !handledUidPlus) {
313                 final ImapFolder newFolder = (ImapFolder)folder;
314                 try {
315                     // Temporarily select the destination folder
316                     newFolder.open(OpenMode.READ_WRITE);
317                     // Do the search(es) ...
318                     for (Message m : messages) {
319                         final String searchString =
320                                 "HEADER Message-Id \"" + m.getMessageId() + "\"";
321                         final String[] newIdArray = newFolder.searchForUids(searchString);
322                         if (newIdArray.length == 1) {
323                             callbacks.onMessageUidChange(m, newIdArray[0]);
324                         }
325                     }
326                 } catch (MessagingException e) {
327                     // Log, but, don't abort; failures here don't need to be propagated
328                     LogUtils.d(Logging.LOG_TAG, "Failed to find message", e);
329                 } finally {
330                     newFolder.close(false);
331                 }
332                 // Re-select the original folder
333                 doSelect();
334             }
335         } catch (IOException ioe) {
336             throw ioExceptionHandler(mConnection, ioe);
337         } finally {
338             destroyResponses();
339         }
340     }
341 
342     @Override
getMessageCount()343     public int getMessageCount() {
344         return mMessageCount;
345     }
346 
347     @Override
getUnreadMessageCount()348     public int getUnreadMessageCount() throws MessagingException {
349         checkOpen();
350         try {
351             int unreadMessageCount = 0;
352             final List<ImapResponse> responses = mConnection.executeSimpleCommand(
353                     String.format(Locale.US,
354                             ImapConstants.STATUS + " \"%s\" (" + ImapConstants.UNSEEN + ")",
355                             ImapStore.encodeFolderName(mName, mStore.mPathPrefix)));
356             // S: * STATUS mboxname (MESSAGES 231 UIDNEXT 44292)
357             for (ImapResponse response : responses) {
358                 if (response.isDataResponse(0, ImapConstants.STATUS)) {
359                     unreadMessageCount = response.getListOrEmpty(2)
360                             .getKeyedStringOrEmpty(ImapConstants.UNSEEN).getNumberOrZero();
361                 }
362             }
363             return unreadMessageCount;
364         } catch (IOException ioe) {
365             throw ioExceptionHandler(mConnection, ioe);
366         } finally {
367             destroyResponses();
368         }
369     }
370 
371     @Override
delete(boolean recurse)372     public void delete(boolean recurse) {
373         throw new Error("ImapStore.delete() not yet implemented");
374     }
375 
getSearchUids(List<ImapResponse> responses)376     String[] getSearchUids(List<ImapResponse> responses) {
377         // S: * SEARCH 2 3 6
378         final ArrayList<String> uids = new ArrayList<String>();
379         for (ImapResponse response : responses) {
380             if (!response.isDataResponse(0, ImapConstants.SEARCH)) {
381                 continue;
382             }
383             // Found SEARCH response data
384             for (int i = 1; i < response.size(); i++) {
385                 ImapString s = response.getStringOrEmpty(i);
386                 if (s.isString()) {
387                     uids.add(s.getString());
388                 }
389             }
390         }
391         return uids.toArray(Utility.EMPTY_STRINGS);
392     }
393 
searchForUids(String searchCriteria)394     String[] searchForUids(String searchCriteria) throws MessagingException {
395         return searchForUids(searchCriteria, true);
396     }
397 
398     /**
399      * I'm not a fan of having a parameter that determines whether to throw exceptions or
400      * consume them, but getMessage() for a date range needs to differentiate between
401      * a failure and just a legitimate empty result.
402      * See b/11183568.
403      * TODO:
404      * Either figure out how to make getMessage() with a date range work without this
405      * exception information, or make all users of searchForUids() handle the ImapException.
406      * It's too late in the release cycle to add this risk right now.
407      */
408     @VisibleForTesting
searchForUids(String searchCriteria, boolean swallowException)409     String[] searchForUids(String searchCriteria, boolean swallowException)
410             throws MessagingException {
411         checkOpen();
412         try {
413             try {
414                 final String command = ImapConstants.UID_SEARCH + " " + searchCriteria;
415                 final String[] result = getSearchUids(mConnection.executeSimpleCommand(command));
416                 LogUtils.d(Logging.LOG_TAG, "searchForUids '" + searchCriteria + "' results: " +
417                         result.length);
418                 return result;
419             } catch (ImapException me) {
420                 LogUtils.d(Logging.LOG_TAG, me, "ImapException in search: " + searchCriteria);
421                 if (swallowException) {
422                     return Utility.EMPTY_STRINGS; // Not found
423                 } else {
424                     throw me;
425                 }
426             } catch (IOException ioe) {
427                 LogUtils.d(Logging.LOG_TAG, ioe, "IOException in search: " + searchCriteria);
428                 throw ioExceptionHandler(mConnection, ioe);
429             }
430         } finally {
431             destroyResponses();
432         }
433     }
434 
435     @Override
436     @VisibleForTesting
getMessage(String uid)437     public Message getMessage(String uid) throws MessagingException {
438         checkOpen();
439 
440         final String[] uids = searchForUids(ImapConstants.UID + " " + uid);
441         for (int i = 0; i < uids.length; i++) {
442             if (uids[i].equals(uid)) {
443                 return new ImapMessage(uid, this);
444             }
445         }
446         return null;
447     }
448 
449     @VisibleForTesting
isAsciiString(String str)450     protected static boolean isAsciiString(String str) {
451         int len = str.length();
452         for (int i = 0; i < len; i++) {
453             char c = str.charAt(i);
454             if (c >= 128) return false;
455         }
456         return true;
457     }
458 
459     /**
460      * Retrieve messages based on search parameters.  We search FROM, TO, CC, SUBJECT, and BODY
461      * We send: SEARCH OR FROM "foo" (OR TO "foo" (OR CC "foo" (OR SUBJECT "foo" BODY "foo"))), but
462      * with the additional CHARSET argument and sending "foo" as a literal (e.g. {3}<CRLF>foo}
463      */
464     @Override
465     @VisibleForTesting
getMessages(SearchParams params, MessageRetrievalListener listener)466     public Message[] getMessages(SearchParams params, MessageRetrievalListener listener)
467             throws MessagingException {
468         List<String> commands = new ArrayList<String>();
469         final String filter = params.mFilter;
470         // All servers MUST accept US-ASCII, so we'll send this as the CHARSET unless we're really
471         // dealing with a string that contains non-ascii characters
472         String charset = "US-ASCII";
473         if (!isAsciiString(filter)) {
474             charset = "UTF-8";
475         }
476         // This is the length of the string in octets (bytes), formatted as a string literal {n}
477         final String octetLength = "{" + filter.getBytes().length + "}";
478         // Break the command up into pieces ending with the string literal length
479         commands.add(ImapConstants.UID_SEARCH + " CHARSET " + charset + " OR FROM " + octetLength);
480         commands.add(filter + " (OR TO " + octetLength);
481         commands.add(filter + " (OR CC " + octetLength);
482         commands.add(filter + " (OR SUBJECT " + octetLength);
483         commands.add(filter + " BODY " + octetLength);
484         commands.add(filter + ")))");
485         return getMessagesInternal(complexSearchForUids(commands), listener);
486     }
487 
complexSearchForUids(List<String> commands)488     /* package */ String[] complexSearchForUids(List<String> commands) throws MessagingException {
489         checkOpen();
490         try {
491             try {
492                 return getSearchUids(mConnection.executeComplexCommand(commands, false));
493             } catch (ImapException e) {
494                 return Utility.EMPTY_STRINGS; // not found;
495             } catch (IOException ioe) {
496                 throw ioExceptionHandler(mConnection, ioe);
497             }
498         } finally {
499             destroyResponses();
500         }
501     }
502 
503     @Override
504     @VisibleForTesting
getMessages(int start, int end, MessageRetrievalListener listener)505     public Message[] getMessages(int start, int end, MessageRetrievalListener listener)
506             throws MessagingException {
507         if (start < 1 || end < 1 || end < start) {
508             throw new MessagingException(String.format("Invalid range: %d %d", start, end));
509         }
510         LogUtils.d(Logging.LOG_TAG, "getMessages number " + start + " - " + end);
511         return getMessagesInternal(
512                 searchForUids(String.format(Locale.US, "%d:%d NOT DELETED", start, end)), listener);
513     }
514 
generateDateRangeCommand(final long startDate, final long endDate, boolean useQuotes)515     private String generateDateRangeCommand(final long startDate, final long endDate,
516             boolean useQuotes)
517             throws MessagingException {
518         // Dates must be formatted like: 7-Feb-1994. Time info within a date is not
519         // universally supported.
520         // XXX can I limit the maximum number of results?
521         final SimpleDateFormat formatter = new SimpleDateFormat("dd-LLL-yyyy", Locale.US);
522         formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
523         final String sinceDateStr = formatter.format(endDate);
524 
525         StringBuilder queryParam = new StringBuilder();
526         queryParam.append( "1:* ");
527         // If the caller requests a startDate of zero, then ignore the BEFORE parameter.
528         // This makes sure that we can always query for the newest messages, even if our
529         // time is different from the imap server's time.
530         if (startDate != 0) {
531             final String beforeDateStr = formatter.format(startDate);
532             if (startDate < endDate) {
533                 throw new MessagingException(String.format("Invalid date range: %s - %s",
534                         sinceDateStr, beforeDateStr));
535             }
536             queryParam.append("BEFORE ");
537             if (useQuotes) queryParam.append('\"');
538             queryParam.append(beforeDateStr);
539             if (useQuotes) queryParam.append('\"');
540             queryParam.append(" ");
541         }
542         queryParam.append("SINCE ");
543         if (useQuotes) queryParam.append('\"');
544         queryParam.append(sinceDateStr);
545         if (useQuotes) queryParam.append('\"');
546 
547         return queryParam.toString();
548     }
549 
550     @Override
551     @VisibleForTesting
getMessages(long startDate, long endDate, MessageRetrievalListener listener)552     public Message[] getMessages(long startDate, long endDate, MessageRetrievalListener listener)
553             throws MessagingException {
554         String [] uids = null;
555         String command = generateDateRangeCommand(startDate, endDate, false);
556         LogUtils.d(Logging.LOG_TAG, "getMessages dateRange " + command.toString());
557 
558         try {
559             uids = searchForUids(command.toString(), false);
560         } catch (ImapException e) {
561             // TODO: This is a last minute hack to make certain servers work. Some servers
562             // demand that the date in the date range be surrounded by double quotes, other
563             // servers won't accept that. So if we can an ImapException using one method,
564             // try the other.
565             // See b/11183568
566             LogUtils.d(Logging.LOG_TAG, e, "query failed %s, trying alternate",
567                     command.toString());
568             command = generateDateRangeCommand(startDate, endDate, true);
569             try {
570                 uids = searchForUids(command, true);
571             } catch (ImapException e2) {
572                 LogUtils.w(Logging.LOG_TAG, e2, "query failed %s, fatal", command);
573                 uids = null;
574             }
575         }
576         return getMessagesInternal(uids, listener);
577     }
578 
579     @Override
580     @VisibleForTesting
getMessages(String[] uids, MessageRetrievalListener listener)581     public Message[] getMessages(String[] uids, MessageRetrievalListener listener)
582             throws MessagingException {
583         if (uids == null) {
584             uids = searchForUids("1:* NOT DELETED");
585         }
586         return getMessagesInternal(uids, listener);
587     }
588 
getMessagesInternal(String[] uids, MessageRetrievalListener listener)589     public Message[] getMessagesInternal(String[] uids, MessageRetrievalListener listener) {
590         final ArrayList<Message> messages = new ArrayList<Message>(uids.length);
591         for (int i = 0; i < uids.length; i++) {
592             final String uid = uids[i];
593             final ImapMessage message = new ImapMessage(uid, this);
594             messages.add(message);
595             if (listener != null) {
596                 listener.messageRetrieved(message);
597             }
598         }
599         return messages.toArray(Message.EMPTY_ARRAY);
600     }
601 
602     @Override
fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)603     public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)
604             throws MessagingException {
605         try {
606             fetchInternal(messages, fp, listener);
607         } catch (RuntimeException e) { // Probably a parser error.
608             LogUtils.w(Logging.LOG_TAG, "Exception detected: " + e.getMessage());
609             if (mConnection != null) {
610                 mConnection.logLastDiscourse();
611             }
612             throw e;
613         }
614     }
615 
fetchInternal(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)616     public void fetchInternal(Message[] messages, FetchProfile fp,
617             MessageRetrievalListener listener) throws MessagingException {
618         if (messages.length == 0) {
619             return;
620         }
621         checkOpen();
622         HashMap<String, Message> messageMap = new HashMap<String, Message>();
623         for (Message m : messages) {
624             messageMap.put(m.getUid(), m);
625         }
626 
627         /*
628          * Figure out what command we are going to run:
629          * FLAGS     - UID FETCH (FLAGS)
630          * ENVELOPE  - UID FETCH (INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[
631          *                            HEADER.FIELDS (date subject from content-type to cc)])
632          * STRUCTURE - UID FETCH (BODYSTRUCTURE)
633          * BODY_SANE - UID FETCH (BODY.PEEK[]<0.N>) where N = max bytes returned
634          * BODY      - UID FETCH (BODY.PEEK[])
635          * Part      - UID FETCH (BODY.PEEK[ID]) where ID = mime part ID
636          */
637 
638         final LinkedHashSet<String> fetchFields = new LinkedHashSet<String>();
639 
640         fetchFields.add(ImapConstants.UID);
641         if (fp.contains(FetchProfile.Item.FLAGS)) {
642             fetchFields.add(ImapConstants.FLAGS);
643         }
644         if (fp.contains(FetchProfile.Item.ENVELOPE)) {
645             fetchFields.add(ImapConstants.INTERNALDATE);
646             fetchFields.add(ImapConstants.RFC822_SIZE);
647             fetchFields.add(ImapConstants.FETCH_FIELD_HEADERS);
648         }
649         if (fp.contains(FetchProfile.Item.STRUCTURE)) {
650             fetchFields.add(ImapConstants.BODYSTRUCTURE);
651         }
652 
653         if (fp.contains(FetchProfile.Item.BODY_SANE)) {
654             fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_SANE);
655         }
656         if (fp.contains(FetchProfile.Item.BODY)) {
657             fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK);
658         }
659 
660         // TODO Why are we only fetching the first part given?
661         final Part fetchPart = fp.getFirstPart();
662         if (fetchPart != null) {
663             final String[] partIds =
664                     fetchPart.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA);
665             // TODO Why can a single part have more than one Id? And why should we only fetch
666             // the first id if there are more than one?
667             if (partIds != null) {
668                 fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_BARE
669                         + "[" + partIds[0] + "]");
670             }
671         }
672 
673         try {
674             mConnection.sendCommand(String.format(Locale.US,
675                     ImapConstants.UID_FETCH + " %s (%s)", ImapStore.joinMessageUids(messages),
676                     Utility.combine(fetchFields.toArray(new String[fetchFields.size()]), ' ')
677                     ), false);
678             ImapResponse response;
679             do {
680                 response = null;
681                 try {
682                     response = mConnection.readResponse();
683 
684                     if (!response.isDataResponse(1, ImapConstants.FETCH)) {
685                         continue; // Ignore
686                     }
687                     final ImapList fetchList = response.getListOrEmpty(2);
688                     final String uid = fetchList.getKeyedStringOrEmpty(ImapConstants.UID)
689                             .getString();
690                     if (TextUtils.isEmpty(uid)) continue;
691 
692                     ImapMessage message = (ImapMessage) messageMap.get(uid);
693                     if (message == null) continue;
694 
695                     if (fp.contains(FetchProfile.Item.FLAGS)) {
696                         final ImapList flags =
697                             fetchList.getKeyedListOrEmpty(ImapConstants.FLAGS);
698                         for (int i = 0, count = flags.size(); i < count; i++) {
699                             final ImapString flag = flags.getStringOrEmpty(i);
700                             if (flag.is(ImapConstants.FLAG_DELETED)) {
701                                 message.setFlagInternal(Flag.DELETED, true);
702                             } else if (flag.is(ImapConstants.FLAG_ANSWERED)) {
703                                 message.setFlagInternal(Flag.ANSWERED, true);
704                             } else if (flag.is(ImapConstants.FLAG_SEEN)) {
705                                 message.setFlagInternal(Flag.SEEN, true);
706                             } else if (flag.is(ImapConstants.FLAG_FLAGGED)) {
707                                 message.setFlagInternal(Flag.FLAGGED, true);
708                             }
709                         }
710                     }
711                     if (fp.contains(FetchProfile.Item.ENVELOPE)) {
712                         final Date internalDate = fetchList.getKeyedStringOrEmpty(
713                                 ImapConstants.INTERNALDATE).getDateOrNull();
714                         final int size = fetchList.getKeyedStringOrEmpty(
715                                 ImapConstants.RFC822_SIZE).getNumberOrZero();
716                         final String header = fetchList.getKeyedStringOrEmpty(
717                                 ImapConstants.BODY_BRACKET_HEADER, true).getString();
718 
719                         message.setInternalDate(internalDate);
720                         message.setSize(size);
721                         message.parse(Utility.streamFromAsciiString(header));
722                     }
723                     if (fp.contains(FetchProfile.Item.STRUCTURE)) {
724                         ImapList bs = fetchList.getKeyedListOrEmpty(
725                                 ImapConstants.BODYSTRUCTURE);
726                         if (!bs.isEmpty()) {
727                             try {
728                                 parseBodyStructure(bs, message, ImapConstants.TEXT);
729                             } catch (MessagingException e) {
730                                 if (Logging.LOGD) {
731                                     LogUtils.v(Logging.LOG_TAG, e, "Error handling message");
732                                 }
733                                 message.setBody(null);
734                             }
735                         }
736                     }
737                     if (fp.contains(FetchProfile.Item.BODY)
738                             || fp.contains(FetchProfile.Item.BODY_SANE)) {
739                         // Body is keyed by "BODY[]...".
740                         // Previously used "BODY[..." but this can be confused with "BODY[HEADER..."
741                         // TODO Should we accept "RFC822" as well??
742                         ImapString body = fetchList.getKeyedStringOrEmpty("BODY[]", true);
743                         InputStream bodyStream = body.getAsStream();
744                         message.parse(bodyStream);
745                     }
746                     if (fetchPart != null) {
747                         InputStream bodyStream =
748                                 fetchList.getKeyedStringOrEmpty("BODY[", true).getAsStream();
749                         String encodings[] = fetchPart.getHeader(
750                                 MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING);
751 
752                         String contentTransferEncoding = null;
753                         if (encodings != null && encodings.length > 0) {
754                             contentTransferEncoding = encodings[0];
755                         } else {
756                             // According to http://tools.ietf.org/html/rfc2045#section-6.1
757                             // "7bit" is the default.
758                             contentTransferEncoding = "7bit";
759                         }
760 
761                         try {
762                             // TODO Don't create 2 temp files.
763                             // decodeBody creates BinaryTempFileBody, but we could avoid this
764                             // if we implement ImapStringBody.
765                             // (We'll need to share a temp file.  Protect it with a ref-count.)
766                             fetchPart.setBody(decodeBody(bodyStream, contentTransferEncoding,
767                                     fetchPart.getSize(), listener));
768                         } catch(Exception e) {
769                             // TODO: Figure out what kinds of exceptions might actually be thrown
770                             // from here. This blanket catch-all is because we're not sure what to
771                             // do if we don't have a contentTransferEncoding, and we don't have
772                             // time to figure out what exceptions might be thrown.
773                             LogUtils.e(Logging.LOG_TAG, "Error fetching body %s", e);
774                         }
775                     }
776 
777                     if (listener != null) {
778                         listener.messageRetrieved(message);
779                     }
780                 } finally {
781                     destroyResponses();
782                 }
783             } while (!response.isTagged());
784         } catch (IOException ioe) {
785             throw ioExceptionHandler(mConnection, ioe);
786         }
787     }
788 
789     /**
790      * Removes any content transfer encoding from the stream and returns a Body.
791      * This code is taken/condensed from MimeUtility.decodeBody
792      */
decodeBody(InputStream in, String contentTransferEncoding, int size, MessageRetrievalListener listener)793     private static Body decodeBody(InputStream in, String contentTransferEncoding, int size,
794             MessageRetrievalListener listener) throws IOException {
795         // Get a properly wrapped input stream
796         in = MimeUtility.getInputStreamForContentTransferEncoding(in, contentTransferEncoding);
797         BinaryTempFileBody tempBody = new BinaryTempFileBody();
798         OutputStream out = tempBody.getOutputStream();
799         try {
800             byte[] buffer = new byte[COPY_BUFFER_SIZE];
801             int n = 0;
802             int count = 0;
803             while (-1 != (n = in.read(buffer))) {
804                 out.write(buffer, 0, n);
805                 count += n;
806                 if (listener != null) {
807                     if (size == 0) {
808                         // We don't know how big the file is, so just fake it.
809                         listener.loadAttachmentProgress((int)Math.ceil(100 * (1-1.0/count)));
810                     } else {
811                         listener.loadAttachmentProgress(count * 100 / size);
812                     }
813                 }
814             }
815         } catch (Base64DataException bde) {
816             String warning = "\n\n" + MailActivityEmail.getMessageDecodeErrorString();
817             out.write(warning.getBytes());
818         } finally {
819             out.close();
820         }
821         return tempBody;
822     }
823 
824     @Override
getPermanentFlags()825     public Flag[] getPermanentFlags() {
826         return PERMANENT_FLAGS;
827     }
828 
829     /**
830      * Handle any untagged responses that the caller doesn't care to handle themselves.
831      * @param responses
832      */
handleUntaggedResponses(List<ImapResponse> responses)833     private void handleUntaggedResponses(List<ImapResponse> responses) {
834         for (ImapResponse response : responses) {
835             handleUntaggedResponse(response);
836         }
837     }
838 
839     /**
840      * Handle an untagged response that the caller doesn't care to handle themselves.
841      * @param response
842      */
handleUntaggedResponse(ImapResponse response)843     private void handleUntaggedResponse(ImapResponse response) {
844         if (response.isDataResponse(1, ImapConstants.EXISTS)) {
845             mMessageCount = response.getStringOrEmpty(0).getNumberOrZero();
846         }
847     }
848 
parseBodyStructure(ImapList bs, Part part, String id)849     private static void parseBodyStructure(ImapList bs, Part part, String id)
850             throws MessagingException {
851         if (bs.getElementOrNone(0).isList()) {
852             /*
853              * This is a multipart/*
854              */
855             MimeMultipart mp = new MimeMultipart();
856             for (int i = 0, count = bs.size(); i < count; i++) {
857                 ImapElement e = bs.getElementOrNone(i);
858                 if (e.isList()) {
859                     /*
860                      * For each part in the message we're going to add a new BodyPart and parse
861                      * into it.
862                      */
863                     MimeBodyPart bp = new MimeBodyPart();
864                     if (id.equals(ImapConstants.TEXT)) {
865                         parseBodyStructure(bs.getListOrEmpty(i), bp, Integer.toString(i + 1));
866 
867                     } else {
868                         parseBodyStructure(bs.getListOrEmpty(i), bp, id + "." + (i + 1));
869                     }
870                     mp.addBodyPart(bp);
871 
872                 } else {
873                     if (e.isString()) {
874                         mp.setSubType(bs.getStringOrEmpty(i).getString().toLowerCase(Locale.US));
875                     }
876                     break; // Ignore the rest of the list.
877                 }
878             }
879             part.setBody(mp);
880         } else {
881             /*
882              * This is a body. We need to add as much information as we can find out about
883              * it to the Part.
884              */
885 
886             /*
887              body type
888              body subtype
889              body parameter parenthesized list
890              body id
891              body description
892              body encoding
893              body size
894              */
895 
896             final ImapString type = bs.getStringOrEmpty(0);
897             final ImapString subType = bs.getStringOrEmpty(1);
898             final String mimeType =
899                     (type.getString() + "/" + subType.getString()).toLowerCase(Locale.US);
900 
901             final ImapList bodyParams = bs.getListOrEmpty(2);
902             final ImapString cid = bs.getStringOrEmpty(3);
903             final ImapString encoding = bs.getStringOrEmpty(5);
904             final int size = bs.getStringOrEmpty(6).getNumberOrZero();
905 
906             if (MimeUtility.mimeTypeMatches(mimeType, MimeUtility.MIME_TYPE_RFC822)) {
907                 // A body type of type MESSAGE and subtype RFC822
908                 // contains, immediately after the basic fields, the
909                 // envelope structure, body structure, and size in
910                 // text lines of the encapsulated message.
911                 // [MESSAGE, RFC822, [NAME, filename.eml], NIL, NIL, 7BIT, 5974, NIL,
912                 //     [INLINE, [FILENAME*0, Fwd: Xxx..., FILENAME*1, filename.eml]], NIL]
913                 /*
914                  * This will be caught by fetch and handled appropriately.
915                  */
916                 throw new MessagingException("BODYSTRUCTURE " + MimeUtility.MIME_TYPE_RFC822
917                         + " not yet supported.");
918             }
919 
920             /*
921              * Set the content type with as much information as we know right now.
922              */
923             final StringBuilder contentType = new StringBuilder(mimeType);
924 
925             /*
926              * If there are body params we might be able to get some more information out
927              * of them.
928              */
929             for (int i = 1, count = bodyParams.size(); i < count; i += 2) {
930 
931                 // TODO We need to convert " into %22, but
932                 // because MimeUtility.getHeaderParameter doesn't recognize it,
933                 // we can't fix it for now.
934                 contentType.append(String.format(";\n %s=\"%s\"",
935                         bodyParams.getStringOrEmpty(i - 1).getString(),
936                         bodyParams.getStringOrEmpty(i).getString()));
937             }
938 
939             part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType.toString());
940 
941             // Extension items
942             final ImapList bodyDisposition;
943 
944             if (type.is(ImapConstants.TEXT) && bs.getElementOrNone(9).isList()) {
945                 // If media-type is TEXT, 9th element might be: [body-fld-lines] := number
946                 // So, if it's not a list, use 10th element.
947                 // (Couldn't find evidence in the RFC if it's ALWAYS 10th element.)
948                 bodyDisposition = bs.getListOrEmpty(9);
949             } else {
950                 bodyDisposition = bs.getListOrEmpty(8);
951             }
952 
953             final StringBuilder contentDisposition = new StringBuilder();
954 
955             if (bodyDisposition.size() > 0) {
956                 final String bodyDisposition0Str =
957                         bodyDisposition.getStringOrEmpty(0).getString().toLowerCase(Locale.US);
958                 if (!TextUtils.isEmpty(bodyDisposition0Str)) {
959                     contentDisposition.append(bodyDisposition0Str);
960                 }
961 
962                 final ImapList bodyDispositionParams = bodyDisposition.getListOrEmpty(1);
963                 if (!bodyDispositionParams.isEmpty()) {
964                     /*
965                      * If there is body disposition information we can pull some more
966                      * information about the attachment out.
967                      */
968                     for (int i = 1, count = bodyDispositionParams.size(); i < count; i += 2) {
969 
970                         // TODO We need to convert " into %22.  See above.
971                         contentDisposition.append(String.format(Locale.US, ";\n %s=\"%s\"",
972                                 bodyDispositionParams.getStringOrEmpty(i - 1)
973                                         .getString().toLowerCase(Locale.US),
974                                 bodyDispositionParams.getStringOrEmpty(i).getString()));
975                     }
976                 }
977             }
978 
979             if ((size > 0)
980                     && (MimeUtility.getHeaderParameter(contentDisposition.toString(), "size")
981                             == null)) {
982                 contentDisposition.append(String.format(Locale.US, ";\n size=%d", size));
983             }
984 
985             if (contentDisposition.length() > 0) {
986                 /*
987                  * Set the content disposition containing at least the size. Attachment
988                  * handling code will use this down the road.
989                  */
990                 part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION,
991                         contentDisposition.toString());
992             }
993 
994             /*
995              * Set the Content-Transfer-Encoding header. Attachment code will use this
996              * to parse the body.
997              */
998             if (!encoding.isEmpty()) {
999                 part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING,
1000                         encoding.getString());
1001             }
1002 
1003             /*
1004              * Set the Content-ID header.
1005              */
1006             if (!cid.isEmpty()) {
1007                 part.setHeader(MimeHeader.HEADER_CONTENT_ID, cid.getString());
1008             }
1009 
1010             if (size > 0) {
1011                 if (part instanceof ImapMessage) {
1012                     ((ImapMessage) part).setSize(size);
1013                 } else if (part instanceof MimeBodyPart) {
1014                     ((MimeBodyPart) part).setSize(size);
1015                 } else {
1016                     throw new MessagingException("Unknown part type " + part.toString());
1017                 }
1018             }
1019             part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id);
1020         }
1021 
1022     }
1023 
1024     /**
1025      * Appends the given messages to the selected folder. This implementation also determines
1026      * the new UID of the given message on the IMAP server and sets the Message's UID to the
1027      * new server UID.
1028      */
1029     @Override
appendMessages(Message[] messages)1030     public void appendMessages(Message[] messages) throws MessagingException {
1031         checkOpen();
1032         try {
1033             for (Message message : messages) {
1034                 // Create output count
1035                 CountingOutputStream out = new CountingOutputStream();
1036                 EOLConvertingOutputStream eolOut = new EOLConvertingOutputStream(out);
1037                 message.writeTo(eolOut);
1038                 eolOut.flush();
1039                 // Create flag list (most often this will be "\SEEN")
1040                 String flagList = "";
1041                 Flag[] flags = message.getFlags();
1042                 if (flags.length > 0) {
1043                     StringBuilder sb = new StringBuilder();
1044                     for (int i = 0, count = flags.length; i < count; i++) {
1045                         Flag flag = flags[i];
1046                         if (flag == Flag.SEEN) {
1047                             sb.append(" " + ImapConstants.FLAG_SEEN);
1048                         } else if (flag == Flag.FLAGGED) {
1049                             sb.append(" " + ImapConstants.FLAG_FLAGGED);
1050                         }
1051                     }
1052                     if (sb.length() > 0) {
1053                         flagList = sb.substring(1);
1054                     }
1055                 }
1056 
1057                 mConnection.sendCommand(
1058                         String.format(Locale.US, ImapConstants.APPEND + " \"%s\" (%s) {%d}",
1059                                 ImapStore.encodeFolderName(mName, mStore.mPathPrefix),
1060                                 flagList,
1061                                 out.getCount()), false);
1062                 ImapResponse response;
1063                 do {
1064                     response = mConnection.readResponse();
1065                     if (response.isContinuationRequest()) {
1066                         eolOut = new EOLConvertingOutputStream(
1067                                 mConnection.mTransport.getOutputStream());
1068                         message.writeTo(eolOut);
1069                         eolOut.write('\r');
1070                         eolOut.write('\n');
1071                         eolOut.flush();
1072                     } else if (!response.isTagged()) {
1073                         handleUntaggedResponse(response);
1074                     }
1075                 } while (!response.isTagged());
1076 
1077                 // TODO Why not check the response?
1078 
1079                 /*
1080                  * Try to recover the UID of the message from an APPENDUID response.
1081                  * e.g. 11 OK [APPENDUID 2 238268] APPEND completed
1082                  */
1083                 final ImapList appendList = response.getListOrEmpty(1);
1084                 if ((appendList.size() >= 3) && appendList.is(0, ImapConstants.APPENDUID)) {
1085                     String serverUid = appendList.getStringOrEmpty(2).getString();
1086                     if (!TextUtils.isEmpty(serverUid)) {
1087                         message.setUid(serverUid);
1088                         continue;
1089                     }
1090                 }
1091 
1092                 /*
1093                  * Try to find the UID of the message we just appended using the
1094                  * Message-ID header.  If there are more than one response, take the
1095                  * last one, as it's most likely the newest (the one we just uploaded).
1096                  */
1097                 final String messageId = message.getMessageId();
1098                 if (messageId == null || messageId.length() == 0) {
1099                     continue;
1100                 }
1101                 // Most servers don't care about parenthesis in the search query [and, some
1102                 // fail to work if they are used]
1103                 String[] uids = searchForUids(
1104                         String.format(Locale.US, "HEADER MESSAGE-ID %s", messageId));
1105                 if (uids.length > 0) {
1106                     message.setUid(uids[0]);
1107                 }
1108                 // However, there's at least one server [AOL] that fails to work unless there
1109                 // are parenthesis, so, try this as a last resort
1110                 uids = searchForUids(String.format(Locale.US, "(HEADER MESSAGE-ID %s)", messageId));
1111                 if (uids.length > 0) {
1112                     message.setUid(uids[0]);
1113                 }
1114             }
1115         } catch (IOException ioe) {
1116             throw ioExceptionHandler(mConnection, ioe);
1117         } finally {
1118             destroyResponses();
1119         }
1120     }
1121 
1122     @Override
expunge()1123     public Message[] expunge() throws MessagingException {
1124         checkOpen();
1125         try {
1126             handleUntaggedResponses(mConnection.executeSimpleCommand(ImapConstants.EXPUNGE));
1127         } catch (IOException ioe) {
1128             throw ioExceptionHandler(mConnection, ioe);
1129         } finally {
1130             destroyResponses();
1131         }
1132         return null;
1133     }
1134 
1135     @Override
setFlags(Message[] messages, Flag[] flags, boolean value)1136     public void setFlags(Message[] messages, Flag[] flags, boolean value)
1137             throws MessagingException {
1138         checkOpen();
1139 
1140         String allFlags = "";
1141         if (flags.length > 0) {
1142             StringBuilder flagList = new StringBuilder();
1143             for (int i = 0, count = flags.length; i < count; i++) {
1144                 Flag flag = flags[i];
1145                 if (flag == Flag.SEEN) {
1146                     flagList.append(" " + ImapConstants.FLAG_SEEN);
1147                 } else if (flag == Flag.DELETED) {
1148                     flagList.append(" " + ImapConstants.FLAG_DELETED);
1149                 } else if (flag == Flag.FLAGGED) {
1150                     flagList.append(" " + ImapConstants.FLAG_FLAGGED);
1151                 } else if (flag == Flag.ANSWERED) {
1152                     flagList.append(" " + ImapConstants.FLAG_ANSWERED);
1153                 }
1154             }
1155             allFlags = flagList.substring(1);
1156         }
1157         try {
1158             mConnection.executeSimpleCommand(String.format(Locale.US,
1159                     ImapConstants.UID_STORE + " %s %s" + ImapConstants.FLAGS_SILENT + " (%s)",
1160                     ImapStore.joinMessageUids(messages),
1161                     value ? "+" : "-",
1162                     allFlags));
1163 
1164         } catch (IOException ioe) {
1165             throw ioExceptionHandler(mConnection, ioe);
1166         } finally {
1167             destroyResponses();
1168         }
1169     }
1170 
1171     /**
1172      * Persists this folder. We will always perform the proper database operation (e.g.
1173      * 'save' or 'update'). As an optimization, if a folder has not been modified, no
1174      * database operations are performed.
1175      */
save(Context context)1176     void save(Context context) {
1177         final Mailbox mailbox = mMailbox;
1178         if (!mailbox.isSaved()) {
1179             mailbox.save(context);
1180             mHash = mailbox.getHashes();
1181         } else {
1182             Object[] hash = mailbox.getHashes();
1183             if (!Arrays.equals(mHash, hash)) {
1184                 mailbox.update(context, mailbox.toContentValues());
1185                 mHash = hash;  // Save updated hash
1186             }
1187         }
1188     }
1189 
1190     /**
1191      * Selects the folder for use. Before performing any operations on this folder, it
1192      * must be selected.
1193      */
doSelect()1194     private void doSelect() throws IOException, MessagingException {
1195         final List<ImapResponse> responses = mConnection.executeSimpleCommand(
1196                 String.format(Locale.US, ImapConstants.SELECT + " \"%s\"",
1197                         ImapStore.encodeFolderName(mName, mStore.mPathPrefix)));
1198 
1199         // Assume the folder is opened read-write; unless we are notified otherwise
1200         mMode = OpenMode.READ_WRITE;
1201         int messageCount = -1;
1202         for (ImapResponse response : responses) {
1203             if (response.isDataResponse(1, ImapConstants.EXISTS)) {
1204                 messageCount = response.getStringOrEmpty(0).getNumberOrZero();
1205             } else if (response.isOk()) {
1206                 final ImapString responseCode = response.getResponseCodeOrEmpty();
1207                 if (responseCode.is(ImapConstants.READ_ONLY)) {
1208                     mMode = OpenMode.READ_ONLY;
1209                 } else if (responseCode.is(ImapConstants.READ_WRITE)) {
1210                     mMode = OpenMode.READ_WRITE;
1211                 }
1212             } else if (response.isTagged()) { // Not OK
1213                 throw new MessagingException("Can't open mailbox: "
1214                         + response.getStatusResponseTextOrEmpty());
1215             }
1216         }
1217         if (messageCount == -1) {
1218             throw new MessagingException("Did not find message count during select");
1219         }
1220         mMessageCount = messageCount;
1221         mExists = true;
1222     }
1223 
checkOpen()1224     private void checkOpen() throws MessagingException {
1225         if (!isOpen()) {
1226             throw new MessagingException("Folder " + mName + " is not open.");
1227         }
1228     }
1229 
ioExceptionHandler(ImapConnection connection, IOException ioe)1230     private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) {
1231         if (MailActivityEmail.DEBUG) {
1232             LogUtils.d(Logging.LOG_TAG, "IO Exception detected: ", ioe);
1233         }
1234         connection.close();
1235         if (connection == mConnection) {
1236             mConnection = null; // To prevent close() from returning the connection to the pool.
1237             close(false);
1238         }
1239         return new MessagingException("IO Error", ioe);
1240     }
1241 
1242     @Override
equals(Object o)1243     public boolean equals(Object o) {
1244         if (o instanceof ImapFolder) {
1245             return ((ImapFolder)o).mName.equals(mName);
1246         }
1247         return super.equals(o);
1248     }
1249 
1250     @Override
createMessage(String uid)1251     public Message createMessage(String uid) {
1252         return new ImapMessage(uid, this);
1253     }
1254 }
1255