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