• 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                         String bodyText = body.getString();
647                         InputStream bodyStream = body.getAsStream();
648                         message.parse(bodyStream);
649                     }
650                     if (fetchPart != null && fetchPart.getSize() > 0) {
651                         InputStream bodyStream =
652                                 fetchList.getKeyedStringOrEmpty("BODY[", true).getAsStream();
653                         String contentType = fetchPart.getContentType();
654                         String contentTransferEncoding = fetchPart.getHeader(
655                                 MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING)[0];
656 
657                         // TODO Don't create 2 temp files.
658                         // decodeBody creates BinaryTempFileBody, but we could avoid this
659                         // if we implement ImapStringBody.
660                         // (We'll need to share a temp file.  Protect it with a ref-count.)
661                         fetchPart.setBody(decodeBody(bodyStream, contentTransferEncoding,
662                                 fetchPart.getSize(), listener));
663                     }
664 
665                     if (listener != null) {
666                         listener.messageRetrieved(message);
667                     }
668                 } finally {
669                     destroyResponses();
670                 }
671             } while (!response.isTagged());
672         } catch (IOException ioe) {
673             throw ioExceptionHandler(mConnection, ioe);
674         }
675     }
676 
677     /**
678      * Removes any content transfer encoding from the stream and returns a Body.
679      * This code is taken/condensed from MimeUtility.decodeBody
680      */
decodeBody(InputStream in, String contentTransferEncoding, int size, MessageRetrievalListener listener)681     private Body decodeBody(InputStream in, String contentTransferEncoding, int size,
682             MessageRetrievalListener listener) throws IOException {
683         // Get a properly wrapped input stream
684         in = MimeUtility.getInputStreamForContentTransferEncoding(in, contentTransferEncoding);
685         BinaryTempFileBody tempBody = new BinaryTempFileBody();
686         OutputStream out = tempBody.getOutputStream();
687         try {
688             byte[] buffer = new byte[COPY_BUFFER_SIZE];
689             int n = 0;
690             int count = 0;
691             while (-1 != (n = in.read(buffer))) {
692                 out.write(buffer, 0, n);
693                 count += n;
694                 if (listener != null) {
695                     listener.loadAttachmentProgress(count * 100 / size);
696                 }
697             }
698         } catch (Base64DataException bde) {
699             String warning = "\n\n" + Email.getMessageDecodeErrorString();
700             out.write(warning.getBytes());
701         } finally {
702             out.close();
703         }
704         return tempBody;
705     }
706 
707     @Override
getPermanentFlags()708     public Flag[] getPermanentFlags() {
709         return PERMANENT_FLAGS;
710     }
711 
712     /**
713      * Handle any untagged responses that the caller doesn't care to handle themselves.
714      * @param responses
715      */
handleUntaggedResponses(List<ImapResponse> responses)716     private void handleUntaggedResponses(List<ImapResponse> responses) {
717         for (ImapResponse response : responses) {
718             handleUntaggedResponse(response);
719         }
720     }
721 
722     /**
723      * Handle an untagged response that the caller doesn't care to handle themselves.
724      * @param response
725      */
handleUntaggedResponse(ImapResponse response)726     private void handleUntaggedResponse(ImapResponse response) {
727         if (response.isDataResponse(1, ImapConstants.EXISTS)) {
728             mMessageCount = response.getStringOrEmpty(0).getNumberOrZero();
729         }
730     }
731 
parseBodyStructure(ImapList bs, Part part, String id)732     private static void parseBodyStructure(ImapList bs, Part part, String id)
733             throws MessagingException {
734         if (bs.getElementOrNone(0).isList()) {
735             /*
736              * This is a multipart/*
737              */
738             MimeMultipart mp = new MimeMultipart();
739             for (int i = 0, count = bs.size(); i < count; i++) {
740                 ImapElement e = bs.getElementOrNone(i);
741                 if (e.isList()) {
742                     /*
743                      * For each part in the message we're going to add a new BodyPart and parse
744                      * into it.
745                      */
746                     MimeBodyPart bp = new MimeBodyPart();
747                     if (id.equals(ImapConstants.TEXT)) {
748                         parseBodyStructure(bs.getListOrEmpty(i), bp, Integer.toString(i + 1));
749 
750                     } else {
751                         parseBodyStructure(bs.getListOrEmpty(i), bp, id + "." + (i + 1));
752                     }
753                     mp.addBodyPart(bp);
754 
755                 } else {
756                     if (e.isString()) {
757                         mp.setSubType(bs.getStringOrEmpty(i).getString().toLowerCase());
758                     }
759                     break; // Ignore the rest of the list.
760                 }
761             }
762             part.setBody(mp);
763         } else {
764             /*
765              * This is a body. We need to add as much information as we can find out about
766              * it to the Part.
767              */
768 
769             /*
770              body type
771              body subtype
772              body parameter parenthesized list
773              body id
774              body description
775              body encoding
776              body size
777              */
778 
779             final ImapString type = bs.getStringOrEmpty(0);
780             final ImapString subType = bs.getStringOrEmpty(1);
781             final String mimeType =
782                     (type.getString() + "/" + subType.getString()).toLowerCase();
783 
784             final ImapList bodyParams = bs.getListOrEmpty(2);
785             final ImapString cid = bs.getStringOrEmpty(3);
786             final ImapString encoding = bs.getStringOrEmpty(5);
787             final int size = bs.getStringOrEmpty(6).getNumberOrZero();
788 
789             if (MimeUtility.mimeTypeMatches(mimeType, MimeUtility.MIME_TYPE_RFC822)) {
790                 // A body type of type MESSAGE and subtype RFC822
791                 // contains, immediately after the basic fields, the
792                 // envelope structure, body structure, and size in
793                 // text lines of the encapsulated message.
794                 // [MESSAGE, RFC822, [NAME, filename.eml], NIL, NIL, 7BIT, 5974, NIL,
795                 //     [INLINE, [FILENAME*0, Fwd: Xxx..., FILENAME*1, filename.eml]], NIL]
796                 /*
797                  * This will be caught by fetch and handled appropriately.
798                  */
799                 throw new MessagingException("BODYSTRUCTURE " + MimeUtility.MIME_TYPE_RFC822
800                         + " not yet supported.");
801             }
802 
803             /*
804              * Set the content type with as much information as we know right now.
805              */
806             final StringBuilder contentType = new StringBuilder(mimeType);
807 
808             /*
809              * If there are body params we might be able to get some more information out
810              * of them.
811              */
812             for (int i = 1, count = bodyParams.size(); i < count; i += 2) {
813 
814                 // TODO We need to convert " into %22, but
815                 // because MimeUtility.getHeaderParameter doesn't recognize it,
816                 // we can't fix it for now.
817                 contentType.append(String.format(";\n %s=\"%s\"",
818                         bodyParams.getStringOrEmpty(i - 1).getString(),
819                         bodyParams.getStringOrEmpty(i).getString()));
820             }
821 
822             part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType.toString());
823 
824             // Extension items
825             final ImapList bodyDisposition;
826 
827             if (type.is(ImapConstants.TEXT) && bs.getElementOrNone(9).isList()) {
828                 // If media-type is TEXT, 9th element might be: [body-fld-lines] := number
829                 // So, if it's not a list, use 10th element.
830                 // (Couldn't find evidence in the RFC if it's ALWAYS 10th element.)
831                 bodyDisposition = bs.getListOrEmpty(9);
832             } else {
833                 bodyDisposition = bs.getListOrEmpty(8);
834             }
835 
836             final StringBuilder contentDisposition = new StringBuilder();
837 
838             if (bodyDisposition.size() > 0) {
839                 final String bodyDisposition0Str =
840                         bodyDisposition.getStringOrEmpty(0).getString().toLowerCase();
841                 if (!TextUtils.isEmpty(bodyDisposition0Str)) {
842                     contentDisposition.append(bodyDisposition0Str);
843                 }
844 
845                 final ImapList bodyDispositionParams = bodyDisposition.getListOrEmpty(1);
846                 if (!bodyDispositionParams.isEmpty()) {
847                     /*
848                      * If there is body disposition information we can pull some more
849                      * information about the attachment out.
850                      */
851                     for (int i = 1, count = bodyDispositionParams.size(); i < count; i += 2) {
852 
853                         // TODO We need to convert " into %22.  See above.
854                         contentDisposition.append(String.format(";\n %s=\"%s\"",
855                                 bodyDispositionParams.getStringOrEmpty(i - 1)
856                                         .getString().toLowerCase(),
857                                 bodyDispositionParams.getStringOrEmpty(i).getString()));
858                     }
859                 }
860             }
861 
862             if ((size > 0)
863                     && (MimeUtility.getHeaderParameter(contentDisposition.toString(), "size")
864                             == null)) {
865                 contentDisposition.append(String.format(";\n size=%d", size));
866             }
867 
868             if (contentDisposition.length() > 0) {
869                 /*
870                  * Set the content disposition containing at least the size. Attachment
871                  * handling code will use this down the road.
872                  */
873                 part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION,
874                         contentDisposition.toString());
875             }
876 
877             /*
878              * Set the Content-Transfer-Encoding header. Attachment code will use this
879              * to parse the body.
880              */
881             if (!encoding.isEmpty()) {
882                 part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING,
883                         encoding.getString());
884             }
885 
886             /*
887              * Set the Content-ID header.
888              */
889             if (!cid.isEmpty()) {
890                 part.setHeader(MimeHeader.HEADER_CONTENT_ID, cid.getString());
891             }
892 
893             if (size > 0) {
894                 if (part instanceof ImapMessage) {
895                     ((ImapMessage) part).setSize(size);
896                 } else if (part instanceof MimeBodyPart) {
897                     ((MimeBodyPart) part).setSize(size);
898                 } else {
899                     throw new MessagingException("Unknown part type " + part.toString());
900                 }
901             }
902             part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id);
903         }
904 
905     }
906 
907     /**
908      * Appends the given messages to the selected folder. This implementation also determines
909      * the new UID of the given message on the IMAP server and sets the Message's UID to the
910      * new server UID.
911      */
912     @Override
appendMessages(Message[] messages)913     public void appendMessages(Message[] messages) throws MessagingException {
914         checkOpen();
915         try {
916             for (Message message : messages) {
917                 // Create output count
918                 CountingOutputStream out = new CountingOutputStream();
919                 EOLConvertingOutputStream eolOut = new EOLConvertingOutputStream(out);
920                 message.writeTo(eolOut);
921                 eolOut.flush();
922                 // Create flag list (most often this will be "\SEEN")
923                 String flagList = "";
924                 Flag[] flags = message.getFlags();
925                 if (flags.length > 0) {
926                     StringBuilder sb = new StringBuilder();
927                     for (int i = 0, count = flags.length; i < count; i++) {
928                         Flag flag = flags[i];
929                         if (flag == Flag.SEEN) {
930                             sb.append(" " + ImapConstants.FLAG_SEEN);
931                         } else if (flag == Flag.FLAGGED) {
932                             sb.append(" " + ImapConstants.FLAG_FLAGGED);
933                         }
934                     }
935                     if (sb.length() > 0) {
936                         flagList = sb.substring(1);
937                     }
938                 }
939 
940                 mConnection.sendCommand(
941                         String.format(ImapConstants.APPEND + " \"%s\" (%s) {%d}",
942                                 ImapStore.encodeFolderName(mName, mStore.mPathPrefix),
943                                 flagList,
944                                 out.getCount()), false);
945                 ImapResponse response;
946                 do {
947                     response = mConnection.readResponse();
948                     if (response.isContinuationRequest()) {
949                         eolOut = new EOLConvertingOutputStream(
950                                 mConnection.mTransport.getOutputStream());
951                         message.writeTo(eolOut);
952                         eolOut.write('\r');
953                         eolOut.write('\n');
954                         eolOut.flush();
955                     } else if (!response.isTagged()) {
956                         handleUntaggedResponse(response);
957                     }
958                 } while (!response.isTagged());
959 
960                 // TODO Why not check the response?
961 
962                 /*
963                  * Try to recover the UID of the message from an APPENDUID response.
964                  * e.g. 11 OK [APPENDUID 2 238268] APPEND completed
965                  */
966                 final ImapList appendList = response.getListOrEmpty(1);
967                 if ((appendList.size() >= 3) && appendList.is(0, ImapConstants.APPENDUID)) {
968                     String serverUid = appendList.getStringOrEmpty(2).getString();
969                     if (!TextUtils.isEmpty(serverUid)) {
970                         message.setUid(serverUid);
971                         continue;
972                     }
973                 }
974 
975                 /*
976                  * Try to find the UID of the message we just appended using the
977                  * Message-ID header.  If there are more than one response, take the
978                  * last one, as it's most likely the newest (the one we just uploaded).
979                  */
980                 String messageId = message.getMessageId();
981                 if (messageId == null || messageId.length() == 0) {
982                     continue;
983                 }
984                 // Most servers don't care about parenthesis in the search query [and, some
985                 // fail to work if they are used]
986                 String[] uids = searchForUids(String.format("HEADER MESSAGE-ID %s", messageId));
987                 if (uids.length > 0) {
988                     message.setUid(uids[0]);
989                 }
990                 // However, there's at least one server [AOL] that fails to work unless there
991                 // are parenthesis, so, try this as a last resort
992                 uids = searchForUids(String.format("(HEADER MESSAGE-ID %s)", messageId));
993                 if (uids.length > 0) {
994                     message.setUid(uids[0]);
995                 }
996             }
997         } catch (IOException ioe) {
998             throw ioExceptionHandler(mConnection, ioe);
999         } finally {
1000             destroyResponses();
1001         }
1002     }
1003 
1004     @Override
expunge()1005     public Message[] expunge() throws MessagingException {
1006         checkOpen();
1007         try {
1008             handleUntaggedResponses(mConnection.executeSimpleCommand(ImapConstants.EXPUNGE));
1009         } catch (IOException ioe) {
1010             throw ioExceptionHandler(mConnection, ioe);
1011         } finally {
1012             destroyResponses();
1013         }
1014         return null;
1015     }
1016 
1017     @Override
setFlags(Message[] messages, Flag[] flags, boolean value)1018     public void setFlags(Message[] messages, Flag[] flags, boolean value)
1019             throws MessagingException {
1020         checkOpen();
1021 
1022         String allFlags = "";
1023         if (flags.length > 0) {
1024             StringBuilder flagList = new StringBuilder();
1025             for (int i = 0, count = flags.length; i < count; i++) {
1026                 Flag flag = flags[i];
1027                 if (flag == Flag.SEEN) {
1028                     flagList.append(" " + ImapConstants.FLAG_SEEN);
1029                 } else if (flag == Flag.DELETED) {
1030                     flagList.append(" " + ImapConstants.FLAG_DELETED);
1031                 } else if (flag == Flag.FLAGGED) {
1032                     flagList.append(" " + ImapConstants.FLAG_FLAGGED);
1033                 } else if (flag == Flag.ANSWERED) {
1034                     flagList.append(" " + ImapConstants.FLAG_ANSWERED);
1035                 }
1036             }
1037             allFlags = flagList.substring(1);
1038         }
1039         try {
1040             mConnection.executeSimpleCommand(String.format(
1041                     ImapConstants.UID_STORE + " %s %s" + ImapConstants.FLAGS_SILENT + " (%s)",
1042                     ImapStore.joinMessageUids(messages),
1043                     value ? "+" : "-",
1044                     allFlags));
1045 
1046         } catch (IOException ioe) {
1047             throw ioExceptionHandler(mConnection, ioe);
1048         } finally {
1049             destroyResponses();
1050         }
1051     }
1052 
1053     /**
1054      * Persists this folder. We will always perform the proper database operation (e.g.
1055      * 'save' or 'update'). As an optimization, if a folder has not been modified, no
1056      * database operations are performed.
1057      */
save(Context context)1058     void save(Context context) {
1059         final Mailbox mailbox = mMailbox;
1060         if (!mailbox.isSaved()) {
1061             mailbox.save(context);
1062             mHash = mailbox.getHashes();
1063         } else {
1064             Object[] hash = mailbox.getHashes();
1065             if (!Arrays.equals(mHash, hash)) {
1066                 mailbox.update(context, mailbox.toContentValues());
1067                 mHash = hash;  // Save updated hash
1068             }
1069         }
1070     }
1071 
1072     /**
1073      * Selects the folder for use. Before performing any operations on this folder, it
1074      * must be selected.
1075      */
doSelect()1076     private void doSelect() throws IOException, MessagingException {
1077         List<ImapResponse> responses = mConnection.executeSimpleCommand(
1078                 String.format(ImapConstants.SELECT + " \"%s\"",
1079                         ImapStore.encodeFolderName(mName, mStore.mPathPrefix)));
1080 
1081         // Assume the folder is opened read-write; unless we are notified otherwise
1082         mMode = OpenMode.READ_WRITE;
1083         int messageCount = -1;
1084         for (ImapResponse response : responses) {
1085             if (response.isDataResponse(1, ImapConstants.EXISTS)) {
1086                 messageCount = response.getStringOrEmpty(0).getNumberOrZero();
1087             } else if (response.isOk()) {
1088                 final ImapString responseCode = response.getResponseCodeOrEmpty();
1089                 if (responseCode.is(ImapConstants.READ_ONLY)) {
1090                     mMode = OpenMode.READ_ONLY;
1091                 } else if (responseCode.is(ImapConstants.READ_WRITE)) {
1092                     mMode = OpenMode.READ_WRITE;
1093                 }
1094             } else if (response.isTagged()) { // Not OK
1095                 throw new MessagingException("Can't open mailbox: "
1096                         + response.getStatusResponseTextOrEmpty());
1097             }
1098         }
1099         if (messageCount == -1) {
1100             throw new MessagingException("Did not find message count during select");
1101         }
1102         mMessageCount = messageCount;
1103         mExists = true;
1104     }
1105 
checkOpen()1106     private void checkOpen() throws MessagingException {
1107         if (!isOpen()) {
1108             throw new MessagingException("Folder " + mName + " is not open.");
1109         }
1110     }
1111 
ioExceptionHandler(ImapConnection connection, IOException ioe)1112     private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) {
1113         if (Email.DEBUG) {
1114             Log.d(Logging.LOG_TAG, "IO Exception detected: ", ioe);
1115         }
1116         connection.close();
1117         if (connection == mConnection) {
1118             mConnection = null; // To prevent close() from returning the connection to the pool.
1119             close(false);
1120         }
1121         return new MessagingException("IO Error", ioe);
1122     }
1123 
1124     @Override
equals(Object o)1125     public boolean equals(Object o) {
1126         if (o instanceof ImapFolder) {
1127             return ((ImapFolder)o).mName.equals(mName);
1128         }
1129         return super.equals(o);
1130     }
1131 
1132     @Override
createMessage(String uid)1133     public Message createMessage(String uid) {
1134         return new ImapMessage(uid, this);
1135     }
1136 }
1137