1 /*
2  * Copyright (C) 2015 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 package com.android.voicemail.impl.mail.store;
17 
18 import android.content.Context;
19 import android.support.annotation.Nullable;
20 import android.support.annotation.VisibleForTesting;
21 import android.text.TextUtils;
22 import android.util.ArrayMap;
23 import android.util.Base64DataException;
24 import com.android.voicemail.impl.OmtpEvents;
25 import com.android.voicemail.impl.VvmLog;
26 import com.android.voicemail.impl.mail.AuthenticationFailedException;
27 import com.android.voicemail.impl.mail.Body;
28 import com.android.voicemail.impl.mail.FetchProfile;
29 import com.android.voicemail.impl.mail.Flag;
30 import com.android.voicemail.impl.mail.Message;
31 import com.android.voicemail.impl.mail.MessagingException;
32 import com.android.voicemail.impl.mail.Part;
33 import com.android.voicemail.impl.mail.internet.BinaryTempFileBody;
34 import com.android.voicemail.impl.mail.internet.MimeBodyPart;
35 import com.android.voicemail.impl.mail.internet.MimeHeader;
36 import com.android.voicemail.impl.mail.internet.MimeMultipart;
37 import com.android.voicemail.impl.mail.internet.MimeUtility;
38 import com.android.voicemail.impl.mail.store.ImapStore.ImapException;
39 import com.android.voicemail.impl.mail.store.ImapStore.ImapMessage;
40 import com.android.voicemail.impl.mail.store.imap.ImapConstants;
41 import com.android.voicemail.impl.mail.store.imap.ImapElement;
42 import com.android.voicemail.impl.mail.store.imap.ImapList;
43 import com.android.voicemail.impl.mail.store.imap.ImapResponse;
44 import com.android.voicemail.impl.mail.store.imap.ImapString;
45 import com.android.voicemail.impl.mail.utils.Utility;
46 import java.io.IOException;
47 import java.io.InputStream;
48 import java.io.OutputStream;
49 import java.util.ArrayList;
50 import java.util.Date;
51 import java.util.LinkedHashSet;
52 import java.util.List;
53 import java.util.Locale;
54 
55 public class ImapFolder {
56   private static final String TAG = "ImapFolder";
57   private static final String[] PERMANENT_FLAGS = {
58     Flag.DELETED, Flag.SEEN, Flag.FLAGGED, Flag.ANSWERED
59   };
60   private static final int COPY_BUFFER_SIZE = 16 * 1024;
61 
62   private final ImapStore store;
63   private final String name;
64   private int messageCount = -1;
65   private ImapConnection connection;
66   private String mode;
67   private boolean exists;
68   /** A set of hashes that can be used to track dirtiness */
69   Object[] hash;
70 
71   public static final String MODE_READ_ONLY = "mode_read_only";
72   public static final String MODE_READ_WRITE = "mode_read_write";
73 
ImapFolder(ImapStore store, String name)74   public ImapFolder(ImapStore store, String name) {
75     this.store = store;
76     this.name = name;
77   }
78 
79   /** Callback for each message retrieval. */
80   public interface MessageRetrievalListener {
messageRetrieved(Message message)81     public void messageRetrieved(Message message);
82   }
83 
destroyResponses()84   private void destroyResponses() {
85     if (connection != null) {
86       connection.destroyResponses();
87     }
88   }
89 
open(String mode)90   public void open(String mode) throws MessagingException {
91     try {
92       if (isOpen()) {
93         throw new AssertionError("Duplicated open on ImapFolder");
94       }
95       synchronized (this) {
96         connection = store.getConnection();
97       }
98       // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft NonJunk
99       // $MDNSent)
100       // * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft
101       // NonJunk $MDNSent \*)] Flags permitted.
102       // * 23 EXISTS
103       // * 0 RECENT
104       // * OK [UIDVALIDITY 1125022061] UIDs valid
105       // * OK [UIDNEXT 57576] Predicted next UID
106       // 2 OK [READ-WRITE] Select completed.
107       try {
108         doSelect();
109       } catch (IOException ioe) {
110         throw ioExceptionHandler(connection, ioe);
111       } finally {
112         destroyResponses();
113       }
114     } catch (AuthenticationFailedException e) {
115       // Don't cache this connection, so we're forced to try connecting/login again
116       connection = null;
117       close(false);
118       throw e;
119     } catch (MessagingException e) {
120       exists = false;
121       close(false);
122       throw e;
123     }
124   }
125 
isOpen()126   public boolean isOpen() {
127     return exists && connection != null;
128   }
129 
getMode()130   public String getMode() {
131     return mode;
132   }
133 
close(boolean expunge)134   public void close(boolean expunge) {
135     if (expunge) {
136       try {
137         expunge();
138       } catch (MessagingException e) {
139         VvmLog.e(TAG, "Messaging Exception", e);
140       }
141     }
142     messageCount = -1;
143     synchronized (this) {
144       connection = null;
145     }
146   }
147 
getMessageCount()148   public int getMessageCount() {
149     return messageCount;
150   }
151 
getSearchUids(List<ImapResponse> responses)152   String[] getSearchUids(List<ImapResponse> responses) {
153     // S: * SEARCH 2 3 6
154     final ArrayList<String> uids = new ArrayList<String>();
155     for (ImapResponse response : responses) {
156       if (!response.isDataResponse(0, ImapConstants.SEARCH)) {
157         continue;
158       }
159       // Found SEARCH response data
160       for (int i = 1; i < response.size(); i++) {
161         ImapString s = response.getStringOrEmpty(i);
162         if (s.isString()) {
163           uids.add(s.getString());
164         }
165       }
166     }
167     return uids.toArray(Utility.EMPTY_STRINGS);
168   }
169 
170   @VisibleForTesting
searchForUids(String searchCriteria)171   String[] searchForUids(String searchCriteria) throws MessagingException {
172     checkOpen();
173     try {
174       try {
175         final String command = ImapConstants.UID_SEARCH + " " + searchCriteria;
176         final String[] result = getSearchUids(connection.executeSimpleCommand(command));
177         VvmLog.d(TAG, "searchForUids '" + searchCriteria + "' results: " + result.length);
178         return result;
179       } catch (ImapException me) {
180         VvmLog.d(TAG, "ImapException in search: " + searchCriteria, me);
181         return Utility.EMPTY_STRINGS; // Not found
182       } catch (IOException ioe) {
183         VvmLog.d(TAG, "IOException in search: " + searchCriteria, ioe);
184         store.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
185         throw ioExceptionHandler(connection, ioe);
186       }
187     } finally {
188       destroyResponses();
189     }
190   }
191 
192   @Nullable
getMessage(String uid)193   public Message getMessage(String uid) throws MessagingException {
194     checkOpen();
195 
196     final String[] uids = searchForUids(ImapConstants.UID + " " + uid);
197     for (int i = 0; i < uids.length; i++) {
198       if (uids[i].equals(uid)) {
199         return new ImapMessage(uid, this);
200       }
201     }
202     VvmLog.e(TAG, "UID " + uid + " not found on server");
203     return null;
204   }
205 
206   @VisibleForTesting
isAsciiString(String str)207   protected static boolean isAsciiString(String str) {
208     int len = str.length();
209     for (int i = 0; i < len; i++) {
210       char c = str.charAt(i);
211       if (c >= 128) return false;
212     }
213     return true;
214   }
215 
getMessages(String[] uids)216   public Message[] getMessages(String[] uids) throws MessagingException {
217     if (uids == null) {
218       uids = searchForUids("1:* NOT DELETED");
219     }
220     return getMessagesInternal(uids);
221   }
222 
getMessagesInternal(String[] uids)223   public Message[] getMessagesInternal(String[] uids) {
224     final ArrayList<Message> messages = new ArrayList<Message>(uids.length);
225     for (int i = 0; i < uids.length; i++) {
226       final String uid = uids[i];
227       final ImapMessage message = new ImapMessage(uid, this);
228       messages.add(message);
229     }
230     return messages.toArray(Message.EMPTY_ARRAY);
231   }
232 
fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)233   public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)
234       throws MessagingException {
235     try {
236       fetchInternal(messages, fp, listener);
237     } catch (RuntimeException e) { // Probably a parser error.
238       VvmLog.w(TAG, "Exception detected: " + e.getMessage());
239       throw e;
240     }
241   }
242 
fetchInternal(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)243   public void fetchInternal(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)
244       throws MessagingException {
245     if (messages.length == 0) {
246       return;
247     }
248     checkOpen();
249     ArrayMap<String, Message> messageMap = new ArrayMap<String, Message>();
250     for (Message m : messages) {
251       messageMap.put(m.getUid(), m);
252     }
253 
254     /*
255      * Figure out what command we are going to run:
256      * FLAGS     - UID FETCH (FLAGS)
257      * ENVELOPE  - UID FETCH (INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[
258      *                            HEADER.FIELDS (date subject from content-type to cc)])
259      * STRUCTURE - UID FETCH (BODYSTRUCTURE)
260      * BODY_TRUNCATED - UID FETCH (BODY.PEEK[]<0.N>) where N = max bytes returned
261      * BODY      - UID FETCH (BODY.PEEK[])
262      * Part      - UID FETCH (BODY.PEEK[ID]) where ID = mime part ID
263      */
264 
265     final LinkedHashSet<String> fetchFields = new LinkedHashSet<String>();
266 
267     fetchFields.add(ImapConstants.UID);
268     if (fp.contains(FetchProfile.Item.FLAGS)) {
269       fetchFields.add(ImapConstants.FLAGS);
270     }
271     if (fp.contains(FetchProfile.Item.ENVELOPE)) {
272       fetchFields.add(ImapConstants.INTERNALDATE);
273       fetchFields.add(ImapConstants.RFC822_SIZE);
274       fetchFields.add(ImapConstants.FETCH_FIELD_HEADERS);
275     }
276     if (fp.contains(FetchProfile.Item.STRUCTURE)) {
277       fetchFields.add(ImapConstants.BODYSTRUCTURE);
278     }
279 
280     if (fp.contains(FetchProfile.Item.BODY_TRUNCATED)) {
281       fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_TRUNCATED);
282     }
283     if (fp.contains(FetchProfile.Item.BODY)) {
284       fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK);
285     }
286 
287     // TODO Why are we only fetching the first part given?
288     final Part fetchPart = fp.getFirstPart();
289     if (fetchPart != null) {
290       final String[] partIds = fetchPart.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA);
291       // TODO Why can a single part have more than one Id? And why should we only fetch
292       // the first id if there are more than one?
293       if (partIds != null) {
294         fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_BARE + "[" + partIds[0] + "]");
295       }
296     }
297 
298     try {
299       connection.sendCommand(
300           String.format(
301               Locale.US,
302               ImapConstants.UID_FETCH + " %s (%s)",
303               ImapStore.joinMessageUids(messages),
304               Utility.combine(fetchFields.toArray(new String[fetchFields.size()]), ' ')),
305           false);
306       ImapResponse response;
307       do {
308         response = null;
309         try {
310           response = connection.readResponse();
311 
312           if (!response.isDataResponse(1, ImapConstants.FETCH)) {
313             continue; // Ignore
314           }
315           final ImapList fetchList = response.getListOrEmpty(2);
316           final String uid = fetchList.getKeyedStringOrEmpty(ImapConstants.UID).getString();
317           if (TextUtils.isEmpty(uid)) continue;
318 
319           ImapMessage message = (ImapMessage) messageMap.get(uid);
320           if (message == null) continue;
321 
322           if (fp.contains(FetchProfile.Item.FLAGS)) {
323             final ImapList flags = fetchList.getKeyedListOrEmpty(ImapConstants.FLAGS);
324             for (int i = 0, count = flags.size(); i < count; i++) {
325               final ImapString flag = flags.getStringOrEmpty(i);
326               if (flag.is(ImapConstants.FLAG_DELETED)) {
327                 message.setFlagInternal(Flag.DELETED, true);
328               } else if (flag.is(ImapConstants.FLAG_ANSWERED)) {
329                 message.setFlagInternal(Flag.ANSWERED, true);
330               } else if (flag.is(ImapConstants.FLAG_SEEN)) {
331                 message.setFlagInternal(Flag.SEEN, true);
332               } else if (flag.is(ImapConstants.FLAG_FLAGGED)) {
333                 message.setFlagInternal(Flag.FLAGGED, true);
334               }
335             }
336           }
337           if (fp.contains(FetchProfile.Item.ENVELOPE)) {
338             final Date internalDate =
339                 fetchList.getKeyedStringOrEmpty(ImapConstants.INTERNALDATE).getDateOrNull();
340             final int size =
341                 fetchList.getKeyedStringOrEmpty(ImapConstants.RFC822_SIZE).getNumberOrZero();
342             final String header =
343                 fetchList
344                     .getKeyedStringOrEmpty(ImapConstants.BODY_BRACKET_HEADER, true)
345                     .getString();
346 
347             message.setInternalDate(internalDate);
348             message.setSize(size);
349             try {
350               message.parse(Utility.streamFromAsciiString(header));
351             } catch (Exception e) {
352               VvmLog.e(TAG, "Error parsing header %s", e);
353             }
354           }
355           if (fp.contains(FetchProfile.Item.STRUCTURE)) {
356             ImapList bs = fetchList.getKeyedListOrEmpty(ImapConstants.BODYSTRUCTURE);
357             if (!bs.isEmpty()) {
358               try {
359                 parseBodyStructure(bs, message, ImapConstants.TEXT);
360               } catch (MessagingException e) {
361                 VvmLog.v(TAG, "Error handling message", e);
362                 message.setBody(null);
363               }
364             }
365           }
366           if (fp.contains(FetchProfile.Item.BODY)
367                   || fp.contains(FetchProfile.Item.BODY_TRUNCATED)) {
368             // Body is keyed by "BODY[]...".
369             // Previously used "BODY[..." but this can be confused with "BODY[HEADER..."
370             // TODO Should we accept "RFC822" as well??
371             ImapString body = fetchList.getKeyedStringOrEmpty("BODY[]", true);
372             InputStream bodyStream = body.getAsStream();
373             try {
374               message.parse(bodyStream);
375             } catch (Exception e) {
376               VvmLog.e(TAG, "Error parsing body %s", e);
377             }
378           }
379           if (fetchPart != null) {
380             InputStream bodyStream = fetchList.getKeyedStringOrEmpty("BODY[", true).getAsStream();
381             String[] encodings = fetchPart.getHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING);
382 
383             String contentTransferEncoding = null;
384             if (encodings != null && encodings.length > 0) {
385               contentTransferEncoding = encodings[0];
386             } else {
387               // According to http://tools.ietf.org/html/rfc2045#section-6.1
388               // "7bit" is the default.
389               contentTransferEncoding = "7bit";
390             }
391 
392             try {
393               // TODO Don't create 2 temp files.
394               // decodeBody creates BinaryTempFileBody, but we could avoid this
395               // if we implement ImapStringBody.
396               // (We'll need to share a temp file.  Protect it with a ref-count.)
397               message.setBody(
398                   decodeBody(
399                       store.getContext(),
400                       bodyStream,
401                       contentTransferEncoding,
402                       fetchPart.getSize(),
403                       listener));
404             } catch (Exception e) {
405               // TODO: Figure out what kinds of exceptions might actually be thrown
406               // from here. This blanket catch-all is because we're not sure what to
407               // do if we don't have a contentTransferEncoding, and we don't have
408               // time to figure out what exceptions might be thrown.
409               VvmLog.e(TAG, "Error fetching body %s", e);
410             }
411           }
412 
413           if (listener != null) {
414             listener.messageRetrieved(message);
415           }
416         } finally {
417           destroyResponses();
418         }
419       } while (!response.isTagged());
420     } catch (IOException ioe) {
421       store.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
422       throw ioExceptionHandler(connection, ioe);
423     }
424   }
425 
426   /**
427    * Removes any content transfer encoding from the stream and returns a Body. This code is
428    * taken/condensed from MimeUtility.decodeBody
429    */
decodeBody( Context context, InputStream in, String contentTransferEncoding, int size, MessageRetrievalListener listener)430   private static Body decodeBody(
431       Context context,
432       InputStream in,
433       String contentTransferEncoding,
434       int size,
435       MessageRetrievalListener listener)
436       throws IOException {
437     // Get a properly wrapped input stream
438     in = MimeUtility.getInputStreamForContentTransferEncoding(in, contentTransferEncoding);
439     BinaryTempFileBody tempBody = new BinaryTempFileBody();
440     OutputStream out = tempBody.getOutputStream();
441     try {
442       byte[] buffer = new byte[COPY_BUFFER_SIZE];
443       int n = 0;
444       int count = 0;
445       while (-1 != (n = in.read(buffer))) {
446         out.write(buffer, 0, n);
447         count += n;
448       }
449     } catch (Base64DataException bde) {
450       String warning = "\n\nThere was an error while decoding the message.";
451       out.write(warning.getBytes());
452     } finally {
453       out.close();
454     }
455     return tempBody;
456   }
457 
getPermanentFlags()458   public String[] getPermanentFlags() {
459     return PERMANENT_FLAGS;
460   }
461 
462   /**
463    * Handle any untagged responses that the caller doesn't care to handle themselves.
464    *
465    * @param responses
466    */
handleUntaggedResponses(List<ImapResponse> responses)467   private void handleUntaggedResponses(List<ImapResponse> responses) {
468     for (ImapResponse response : responses) {
469       handleUntaggedResponse(response);
470     }
471   }
472 
473   /**
474    * Handle an untagged response that the caller doesn't care to handle themselves.
475    *
476    * @param response
477    */
handleUntaggedResponse(ImapResponse response)478   private void handleUntaggedResponse(ImapResponse response) {
479     if (response.isDataResponse(1, ImapConstants.EXISTS)) {
480       messageCount = response.getStringOrEmpty(0).getNumberOrZero();
481     }
482   }
483 
parseBodyStructure(ImapList bs, Part part, String id)484   private static void parseBodyStructure(ImapList bs, Part part, String id)
485       throws MessagingException {
486     if (bs.getElementOrNone(0).isList()) {
487       /*
488        * This is a multipart/*
489        */
490       MimeMultipart mp = new MimeMultipart();
491       for (int i = 0, count = bs.size(); i < count; i++) {
492         ImapElement e = bs.getElementOrNone(i);
493         if (e.isList()) {
494           /*
495            * For each part in the message we're going to add a new BodyPart and parse
496            * into it.
497            */
498           MimeBodyPart bp = new MimeBodyPart();
499           if (id.equals(ImapConstants.TEXT)) {
500             parseBodyStructure(bs.getListOrEmpty(i), bp, Integer.toString(i + 1));
501 
502           } else {
503             parseBodyStructure(bs.getListOrEmpty(i), bp, id + "." + (i + 1));
504           }
505           mp.addBodyPart(bp);
506 
507         } else {
508           if (e.isString()) {
509             mp.setSubType(bs.getStringOrEmpty(i).getString().toLowerCase(Locale.US));
510           }
511           break; // Ignore the rest of the list.
512         }
513       }
514       part.setBody(mp);
515     } else {
516       /*
517        * This is a body. We need to add as much information as we can find out about
518        * it to the Part.
519        */
520 
521       /*
522       body type
523       body subtype
524       body parameter parenthesized list
525       body id
526       body description
527       body encoding
528       body size
529       */
530 
531       final ImapString type = bs.getStringOrEmpty(0);
532       final ImapString subType = bs.getStringOrEmpty(1);
533       final String mimeType = (type.getString() + "/" + subType.getString()).toLowerCase(Locale.US);
534 
535       final ImapList bodyParams = bs.getListOrEmpty(2);
536       final ImapString cid = bs.getStringOrEmpty(3);
537       final ImapString encoding = bs.getStringOrEmpty(5);
538       final int size = bs.getStringOrEmpty(6).getNumberOrZero();
539 
540       if (MimeUtility.mimeTypeMatches(mimeType, MimeUtility.MIME_TYPE_RFC822)) {
541         // A body type of type MESSAGE and subtype RFC822
542         // contains, immediately after the basic fields, the
543         // envelope structure, body structure, and size in
544         // text lines of the encapsulated message.
545         // [MESSAGE, RFC822, [NAME, filename.eml], NIL, NIL, 7BIT, 5974, NIL,
546         //     [INLINE, [FILENAME*0, Fwd: Xxx..., FILENAME*1, filename.eml]], NIL]
547         /*
548          * This will be caught by fetch and handled appropriately.
549          */
550         throw new MessagingException(
551             "BODYSTRUCTURE " + MimeUtility.MIME_TYPE_RFC822 + " not yet supported.");
552       }
553 
554       /*
555        * Set the content type with as much information as we know right now.
556        */
557       final StringBuilder contentType = new StringBuilder(mimeType);
558 
559       /*
560        * If there are body params we might be able to get some more information out
561        * of them.
562        */
563       for (int i = 1, count = bodyParams.size(); i < count; i += 2) {
564 
565         // TODO We need to convert " into %22, but
566         // because MimeUtility.getHeaderParameter doesn't recognize it,
567         // we can't fix it for now.
568         contentType.append(
569             String.format(
570                 ";\n %s=\"%s\"",
571                 bodyParams.getStringOrEmpty(i - 1).getString(),
572                 bodyParams.getStringOrEmpty(i).getString()));
573       }
574 
575       part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType.toString());
576 
577       // Extension items
578       final ImapList bodyDisposition;
579 
580       if (type.is(ImapConstants.TEXT) && bs.getElementOrNone(9).isList()) {
581         // If media-type is TEXT, 9th element might be: [body-fld-lines] := number
582         // So, if it's not a list, use 10th element.
583         // (Couldn't find evidence in the RFC if it's ALWAYS 10th element.)
584         bodyDisposition = bs.getListOrEmpty(9);
585       } else {
586         bodyDisposition = bs.getListOrEmpty(8);
587       }
588 
589       final StringBuilder contentDisposition = new StringBuilder();
590 
591       if (bodyDisposition.size() > 0) {
592         final String bodyDisposition0Str =
593             bodyDisposition.getStringOrEmpty(0).getString().toLowerCase(Locale.US);
594         if (!TextUtils.isEmpty(bodyDisposition0Str)) {
595           contentDisposition.append(bodyDisposition0Str);
596         }
597 
598         final ImapList bodyDispositionParams = bodyDisposition.getListOrEmpty(1);
599         if (!bodyDispositionParams.isEmpty()) {
600           /*
601            * If there is body disposition information we can pull some more
602            * information about the attachment out.
603            */
604           for (int i = 1, count = bodyDispositionParams.size(); i < count; i += 2) {
605 
606             // TODO We need to convert " into %22.  See above.
607             contentDisposition.append(
608                 String.format(
609                     Locale.US,
610                     ";\n %s=\"%s\"",
611                     bodyDispositionParams
612                         .getStringOrEmpty(i - 1)
613                         .getString()
614                         .toLowerCase(Locale.US),
615                     bodyDispositionParams.getStringOrEmpty(i).getString()));
616           }
617         }
618       }
619 
620       if ((size > 0)
621           && (MimeUtility.getHeaderParameter(contentDisposition.toString(), "size") == null)) {
622         contentDisposition.append(String.format(Locale.US, ";\n size=%d", size));
623       }
624 
625       if (contentDisposition.length() > 0) {
626         /*
627          * Set the content disposition containing at least the size. Attachment
628          * handling code will use this down the road.
629          */
630         part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, contentDisposition.toString());
631       }
632 
633       /*
634        * Set the Content-Transfer-Encoding header. Attachment code will use this
635        * to parse the body.
636        */
637       if (!encoding.isEmpty()) {
638         part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, encoding.getString());
639       }
640 
641       /*
642        * Set the Content-ID header.
643        */
644       if (!cid.isEmpty()) {
645         part.setHeader(MimeHeader.HEADER_CONTENT_ID, cid.getString());
646       }
647 
648       if (size > 0) {
649         if (part instanceof ImapMessage) {
650           ((ImapMessage) part).setSize(size);
651         } else if (part instanceof MimeBodyPart) {
652           ((MimeBodyPart) part).setSize(size);
653         } else {
654           throw new MessagingException("Unknown part type " + part.toString());
655         }
656       }
657       part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id);
658     }
659   }
660 
expunge()661   public Message[] expunge() throws MessagingException {
662     checkOpen();
663     try {
664       handleUntaggedResponses(connection.executeSimpleCommand(ImapConstants.EXPUNGE));
665     } catch (IOException ioe) {
666       store.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
667       throw ioExceptionHandler(connection, ioe);
668     } finally {
669       destroyResponses();
670     }
671     return null;
672   }
673 
setFlags(Message[] messages, String[] flags, boolean value)674   public void setFlags(Message[] messages, String[] flags, boolean value)
675       throws MessagingException {
676     checkOpen();
677 
678     String allFlags = "";
679     if (flags.length > 0) {
680       StringBuilder flagList = new StringBuilder();
681       for (int i = 0, count = flags.length; i < count; i++) {
682         String flag = flags[i];
683         if (flag == Flag.SEEN) {
684           flagList.append(" " + ImapConstants.FLAG_SEEN);
685         } else if (flag == Flag.DELETED) {
686           flagList.append(" " + ImapConstants.FLAG_DELETED);
687         } else if (flag == Flag.FLAGGED) {
688           flagList.append(" " + ImapConstants.FLAG_FLAGGED);
689         } else if (flag == Flag.ANSWERED) {
690           flagList.append(" " + ImapConstants.FLAG_ANSWERED);
691         }
692       }
693       allFlags = flagList.substring(1);
694     }
695     try {
696       connection.executeSimpleCommand(
697           String.format(
698               Locale.US,
699               ImapConstants.UID_STORE + " %s %s" + ImapConstants.FLAGS_SILENT + " (%s)",
700               ImapStore.joinMessageUids(messages),
701               value ? "+" : "-",
702               allFlags));
703 
704     } catch (IOException ioe) {
705       store.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
706       throw ioExceptionHandler(connection, ioe);
707     } finally {
708       destroyResponses();
709     }
710   }
711 
712   /**
713    * Selects the folder for use. Before performing any operations on this folder, it must be
714    * selected.
715    */
doSelect()716   private void doSelect() throws IOException, MessagingException {
717     final List<ImapResponse> responses =
718         connection.executeSimpleCommand(
719             String.format(Locale.US, ImapConstants.SELECT + " \"%s\"", name));
720 
721     // Assume the folder is opened read-write; unless we are notified otherwise
722     mode = MODE_READ_WRITE;
723     int messageCount = -1;
724     for (ImapResponse response : responses) {
725       if (response.isDataResponse(1, ImapConstants.EXISTS)) {
726         messageCount = response.getStringOrEmpty(0).getNumberOrZero();
727       } else if (response.isOk()) {
728         final ImapString responseCode = response.getResponseCodeOrEmpty();
729         if (responseCode.is(ImapConstants.READ_ONLY)) {
730           mode = MODE_READ_ONLY;
731         } else if (responseCode.is(ImapConstants.READ_WRITE)) {
732           mode = MODE_READ_WRITE;
733         }
734       } else if (response.isTagged()) { // Not OK
735         store.getImapHelper().handleEvent(OmtpEvents.DATA_MAILBOX_OPEN_FAILED);
736         throw new MessagingException(
737             "Can't open mailbox: " + response.getStatusResponseTextOrEmpty());
738       }
739     }
740     if (messageCount == -1) {
741       throw new MessagingException("Did not find message count during select");
742     }
743     this.messageCount = messageCount;
744     exists = true;
745   }
746 
747   public class Quota {
748 
749     public final int occupied;
750     public final int total;
751 
Quota(int occupied, int total)752     public Quota(int occupied, int total) {
753       this.occupied = occupied;
754       this.total = total;
755     }
756   }
757 
getQuota()758   public Quota getQuota() throws MessagingException {
759     try {
760       final List<ImapResponse> responses =
761           connection.executeSimpleCommand(
762               String.format(Locale.US, ImapConstants.GETQUOTAROOT + " \"%s\"", name));
763 
764       for (ImapResponse response : responses) {
765         if (!response.isDataResponse(0, ImapConstants.QUOTA)) {
766           continue;
767         }
768         ImapList list = response.getListOrEmpty(2);
769         for (int i = 0; i < list.size(); i += 3) {
770           if (!list.getStringOrEmpty(i).is("voice")) {
771             continue;
772           }
773           return new Quota(
774               list.getStringOrEmpty(i + 1).getNumber(-1),
775               list.getStringOrEmpty(i + 2).getNumber(-1));
776         }
777       }
778     } catch (IOException ioe) {
779       store.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
780       throw ioExceptionHandler(connection, ioe);
781     } finally {
782       destroyResponses();
783     }
784     return null;
785   }
786 
checkOpen()787   private void checkOpen() throws MessagingException {
788     if (!isOpen()) {
789       throw new MessagingException("Folder " + name + " is not open.");
790     }
791   }
792 
ioExceptionHandler(ImapConnection connection, IOException ioe)793   private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) {
794     VvmLog.d(TAG, "IO Exception detected: ", ioe);
795     connection.close();
796     if (connection == this.connection) {
797       this.connection = null; // To prevent close() from returning the connection to the pool.
798       close(false);
799     }
800     return new MessagingException(MessagingException.IOERROR, "IO Error", ioe);
801   }
802 
createMessage(String uid)803   public Message createMessage(String uid) {
804     return new ImapMessage(uid, this);
805   }
806 }
807