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