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