• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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_SANE - 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_SANE)) {
281       fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_SANE);
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) || fp.contains(FetchProfile.Item.BODY_SANE)) {
367             // Body is keyed by "BODY[]...".
368             // Previously used "BODY[..." but this can be confused with "BODY[HEADER..."
369             // TODO Should we accept "RFC822" as well??
370             ImapString body = fetchList.getKeyedStringOrEmpty("BODY[]", true);
371             InputStream bodyStream = body.getAsStream();
372             try {
373               message.parse(bodyStream);
374             } catch (Exception e) {
375               VvmLog.e(TAG, "Error parsing body %s", e);
376             }
377           }
378           if (fetchPart != null) {
379             InputStream bodyStream = fetchList.getKeyedStringOrEmpty("BODY[", true).getAsStream();
380             String[] encodings = fetchPart.getHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING);
381 
382             String contentTransferEncoding = null;
383             if (encodings != null && encodings.length > 0) {
384               contentTransferEncoding = encodings[0];
385             } else {
386               // According to http://tools.ietf.org/html/rfc2045#section-6.1
387               // "7bit" is the default.
388               contentTransferEncoding = "7bit";
389             }
390 
391             try {
392               // TODO Don't create 2 temp files.
393               // decodeBody creates BinaryTempFileBody, but we could avoid this
394               // if we implement ImapStringBody.
395               // (We'll need to share a temp file.  Protect it with a ref-count.)
396               message.setBody(
397                   decodeBody(
398                       store.getContext(),
399                       bodyStream,
400                       contentTransferEncoding,
401                       fetchPart.getSize(),
402                       listener));
403             } catch (Exception e) {
404               // TODO: Figure out what kinds of exceptions might actually be thrown
405               // from here. This blanket catch-all is because we're not sure what to
406               // do if we don't have a contentTransferEncoding, and we don't have
407               // time to figure out what exceptions might be thrown.
408               VvmLog.e(TAG, "Error fetching body %s", e);
409             }
410           }
411 
412           if (listener != null) {
413             listener.messageRetrieved(message);
414           }
415         } finally {
416           destroyResponses();
417         }
418       } while (!response.isTagged());
419     } catch (IOException ioe) {
420       store.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
421       throw ioExceptionHandler(connection, ioe);
422     }
423   }
424 
425   /**
426    * Removes any content transfer encoding from the stream and returns a Body. This code is
427    * taken/condensed from MimeUtility.decodeBody
428    */
decodeBody( Context context, InputStream in, String contentTransferEncoding, int size, MessageRetrievalListener listener)429   private static Body decodeBody(
430       Context context,
431       InputStream in,
432       String contentTransferEncoding,
433       int size,
434       MessageRetrievalListener listener)
435       throws IOException {
436     // Get a properly wrapped input stream
437     in = MimeUtility.getInputStreamForContentTransferEncoding(in, contentTransferEncoding);
438     BinaryTempFileBody tempBody = new BinaryTempFileBody();
439     OutputStream out = tempBody.getOutputStream();
440     try {
441       byte[] buffer = new byte[COPY_BUFFER_SIZE];
442       int n = 0;
443       int count = 0;
444       while (-1 != (n = in.read(buffer))) {
445         out.write(buffer, 0, n);
446         count += n;
447       }
448     } catch (Base64DataException bde) {
449       String warning = "\n\nThere was an error while decoding the message.";
450       out.write(warning.getBytes());
451     } finally {
452       out.close();
453     }
454     return tempBody;
455   }
456 
getPermanentFlags()457   public String[] getPermanentFlags() {
458     return PERMANENT_FLAGS;
459   }
460 
461   /**
462    * Handle any untagged responses that the caller doesn't care to handle themselves.
463    *
464    * @param responses
465    */
handleUntaggedResponses(List<ImapResponse> responses)466   private void handleUntaggedResponses(List<ImapResponse> responses) {
467     for (ImapResponse response : responses) {
468       handleUntaggedResponse(response);
469     }
470   }
471 
472   /**
473    * Handle an untagged response that the caller doesn't care to handle themselves.
474    *
475    * @param response
476    */
handleUntaggedResponse(ImapResponse response)477   private void handleUntaggedResponse(ImapResponse response) {
478     if (response.isDataResponse(1, ImapConstants.EXISTS)) {
479       messageCount = response.getStringOrEmpty(0).getNumberOrZero();
480     }
481   }
482 
parseBodyStructure(ImapList bs, Part part, String id)483   private static void parseBodyStructure(ImapList bs, Part part, String id)
484       throws MessagingException {
485     if (bs.getElementOrNone(0).isList()) {
486       /*
487        * This is a multipart/*
488        */
489       MimeMultipart mp = new MimeMultipart();
490       for (int i = 0, count = bs.size(); i < count; i++) {
491         ImapElement e = bs.getElementOrNone(i);
492         if (e.isList()) {
493           /*
494            * For each part in the message we're going to add a new BodyPart and parse
495            * into it.
496            */
497           MimeBodyPart bp = new MimeBodyPart();
498           if (id.equals(ImapConstants.TEXT)) {
499             parseBodyStructure(bs.getListOrEmpty(i), bp, Integer.toString(i + 1));
500 
501           } else {
502             parseBodyStructure(bs.getListOrEmpty(i), bp, id + "." + (i + 1));
503           }
504           mp.addBodyPart(bp);
505 
506         } else {
507           if (e.isString()) {
508             mp.setSubType(bs.getStringOrEmpty(i).getString().toLowerCase(Locale.US));
509           }
510           break; // Ignore the rest of the list.
511         }
512       }
513       part.setBody(mp);
514     } else {
515       /*
516        * This is a body. We need to add as much information as we can find out about
517        * it to the Part.
518        */
519 
520       /*
521       body type
522       body subtype
523       body parameter parenthesized list
524       body id
525       body description
526       body encoding
527       body size
528       */
529 
530       final ImapString type = bs.getStringOrEmpty(0);
531       final ImapString subType = bs.getStringOrEmpty(1);
532       final String mimeType = (type.getString() + "/" + subType.getString()).toLowerCase(Locale.US);
533 
534       final ImapList bodyParams = bs.getListOrEmpty(2);
535       final ImapString cid = bs.getStringOrEmpty(3);
536       final ImapString encoding = bs.getStringOrEmpty(5);
537       final int size = bs.getStringOrEmpty(6).getNumberOrZero();
538 
539       if (MimeUtility.mimeTypeMatches(mimeType, MimeUtility.MIME_TYPE_RFC822)) {
540         // A body type of type MESSAGE and subtype RFC822
541         // contains, immediately after the basic fields, the
542         // envelope structure, body structure, and size in
543         // text lines of the encapsulated message.
544         // [MESSAGE, RFC822, [NAME, filename.eml], NIL, NIL, 7BIT, 5974, NIL,
545         //     [INLINE, [FILENAME*0, Fwd: Xxx..., FILENAME*1, filename.eml]], NIL]
546         /*
547          * This will be caught by fetch and handled appropriately.
548          */
549         throw new MessagingException(
550             "BODYSTRUCTURE " + MimeUtility.MIME_TYPE_RFC822 + " not yet supported.");
551       }
552 
553       /*
554        * Set the content type with as much information as we know right now.
555        */
556       final StringBuilder contentType = new StringBuilder(mimeType);
557 
558       /*
559        * If there are body params we might be able to get some more information out
560        * of them.
561        */
562       for (int i = 1, count = bodyParams.size(); i < count; i += 2) {
563 
564         // TODO We need to convert " into %22, but
565         // because MimeUtility.getHeaderParameter doesn't recognize it,
566         // we can't fix it for now.
567         contentType.append(
568             String.format(
569                 ";\n %s=\"%s\"",
570                 bodyParams.getStringOrEmpty(i - 1).getString(),
571                 bodyParams.getStringOrEmpty(i).getString()));
572       }
573 
574       part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType.toString());
575 
576       // Extension items
577       final ImapList bodyDisposition;
578 
579       if (type.is(ImapConstants.TEXT) && bs.getElementOrNone(9).isList()) {
580         // If media-type is TEXT, 9th element might be: [body-fld-lines] := number
581         // So, if it's not a list, use 10th element.
582         // (Couldn't find evidence in the RFC if it's ALWAYS 10th element.)
583         bodyDisposition = bs.getListOrEmpty(9);
584       } else {
585         bodyDisposition = bs.getListOrEmpty(8);
586       }
587 
588       final StringBuilder contentDisposition = new StringBuilder();
589 
590       if (bodyDisposition.size() > 0) {
591         final String bodyDisposition0Str =
592             bodyDisposition.getStringOrEmpty(0).getString().toLowerCase(Locale.US);
593         if (!TextUtils.isEmpty(bodyDisposition0Str)) {
594           contentDisposition.append(bodyDisposition0Str);
595         }
596 
597         final ImapList bodyDispositionParams = bodyDisposition.getListOrEmpty(1);
598         if (!bodyDispositionParams.isEmpty()) {
599           /*
600            * If there is body disposition information we can pull some more
601            * information about the attachment out.
602            */
603           for (int i = 1, count = bodyDispositionParams.size(); i < count; i += 2) {
604 
605             // TODO We need to convert " into %22.  See above.
606             contentDisposition.append(
607                 String.format(
608                     Locale.US,
609                     ";\n %s=\"%s\"",
610                     bodyDispositionParams
611                         .getStringOrEmpty(i - 1)
612                         .getString()
613                         .toLowerCase(Locale.US),
614                     bodyDispositionParams.getStringOrEmpty(i).getString()));
615           }
616         }
617       }
618 
619       if ((size > 0)
620           && (MimeUtility.getHeaderParameter(contentDisposition.toString(), "size") == null)) {
621         contentDisposition.append(String.format(Locale.US, ";\n size=%d", size));
622       }
623 
624       if (contentDisposition.length() > 0) {
625         /*
626          * Set the content disposition containing at least the size. Attachment
627          * handling code will use this down the road.
628          */
629         part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, contentDisposition.toString());
630       }
631 
632       /*
633        * Set the Content-Transfer-Encoding header. Attachment code will use this
634        * to parse the body.
635        */
636       if (!encoding.isEmpty()) {
637         part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, encoding.getString());
638       }
639 
640       /*
641        * Set the Content-ID header.
642        */
643       if (!cid.isEmpty()) {
644         part.setHeader(MimeHeader.HEADER_CONTENT_ID, cid.getString());
645       }
646 
647       if (size > 0) {
648         if (part instanceof ImapMessage) {
649           ((ImapMessage) part).setSize(size);
650         } else if (part instanceof MimeBodyPart) {
651           ((MimeBodyPart) part).setSize(size);
652         } else {
653           throw new MessagingException("Unknown part type " + part.toString());
654         }
655       }
656       part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id);
657     }
658   }
659 
expunge()660   public Message[] expunge() throws MessagingException {
661     checkOpen();
662     try {
663       handleUntaggedResponses(connection.executeSimpleCommand(ImapConstants.EXPUNGE));
664     } catch (IOException ioe) {
665       store.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
666       throw ioExceptionHandler(connection, ioe);
667     } finally {
668       destroyResponses();
669     }
670     return null;
671   }
672 
setFlags(Message[] messages, String[] flags, boolean value)673   public void setFlags(Message[] messages, String[] flags, boolean value)
674       throws MessagingException {
675     checkOpen();
676 
677     String allFlags = "";
678     if (flags.length > 0) {
679       StringBuilder flagList = new StringBuilder();
680       for (int i = 0, count = flags.length; i < count; i++) {
681         String flag = flags[i];
682         if (flag == Flag.SEEN) {
683           flagList.append(" " + ImapConstants.FLAG_SEEN);
684         } else if (flag == Flag.DELETED) {
685           flagList.append(" " + ImapConstants.FLAG_DELETED);
686         } else if (flag == Flag.FLAGGED) {
687           flagList.append(" " + ImapConstants.FLAG_FLAGGED);
688         } else if (flag == Flag.ANSWERED) {
689           flagList.append(" " + ImapConstants.FLAG_ANSWERED);
690         }
691       }
692       allFlags = flagList.substring(1);
693     }
694     try {
695       connection.executeSimpleCommand(
696           String.format(
697               Locale.US,
698               ImapConstants.UID_STORE + " %s %s" + ImapConstants.FLAGS_SILENT + " (%s)",
699               ImapStore.joinMessageUids(messages),
700               value ? "+" : "-",
701               allFlags));
702 
703     } catch (IOException ioe) {
704       store.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
705       throw ioExceptionHandler(connection, ioe);
706     } finally {
707       destroyResponses();
708     }
709   }
710 
711   /**
712    * Selects the folder for use. Before performing any operations on this folder, it must be
713    * selected.
714    */
doSelect()715   private void doSelect() throws IOException, MessagingException {
716     final List<ImapResponse> responses =
717         connection.executeSimpleCommand(
718             String.format(Locale.US, ImapConstants.SELECT + " \"%s\"", name));
719 
720     // Assume the folder is opened read-write; unless we are notified otherwise
721     mode = MODE_READ_WRITE;
722     int messageCount = -1;
723     for (ImapResponse response : responses) {
724       if (response.isDataResponse(1, ImapConstants.EXISTS)) {
725         messageCount = response.getStringOrEmpty(0).getNumberOrZero();
726       } else if (response.isOk()) {
727         final ImapString responseCode = response.getResponseCodeOrEmpty();
728         if (responseCode.is(ImapConstants.READ_ONLY)) {
729           mode = MODE_READ_ONLY;
730         } else if (responseCode.is(ImapConstants.READ_WRITE)) {
731           mode = MODE_READ_WRITE;
732         }
733       } else if (response.isTagged()) { // Not OK
734         store.getImapHelper().handleEvent(OmtpEvents.DATA_MAILBOX_OPEN_FAILED);
735         throw new MessagingException(
736             "Can't open mailbox: " + response.getStatusResponseTextOrEmpty());
737       }
738     }
739     if (messageCount == -1) {
740       throw new MessagingException("Did not find message count during select");
741     }
742     this.messageCount = messageCount;
743     exists = true;
744   }
745 
746   public class Quota {
747 
748     public final int occupied;
749     public final int total;
750 
Quota(int occupied, int total)751     public Quota(int occupied, int total) {
752       this.occupied = occupied;
753       this.total = total;
754     }
755   }
756 
getQuota()757   public Quota getQuota() throws MessagingException {
758     try {
759       final List<ImapResponse> responses =
760           connection.executeSimpleCommand(
761               String.format(Locale.US, ImapConstants.GETQUOTAROOT + " \"%s\"", name));
762 
763       for (ImapResponse response : responses) {
764         if (!response.isDataResponse(0, ImapConstants.QUOTA)) {
765           continue;
766         }
767         ImapList list = response.getListOrEmpty(2);
768         for (int i = 0; i < list.size(); i += 3) {
769           if (!list.getStringOrEmpty(i).is("voice")) {
770             continue;
771           }
772           return new Quota(
773               list.getStringOrEmpty(i + 1).getNumber(-1),
774               list.getStringOrEmpty(i + 2).getNumber(-1));
775         }
776       }
777     } catch (IOException ioe) {
778       store.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
779       throw ioExceptionHandler(connection, ioe);
780     } finally {
781       destroyResponses();
782     }
783     return null;
784   }
785 
checkOpen()786   private void checkOpen() throws MessagingException {
787     if (!isOpen()) {
788       throw new MessagingException("Folder " + name + " is not open.");
789     }
790   }
791 
ioExceptionHandler(ImapConnection connection, IOException ioe)792   private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) {
793     VvmLog.d(TAG, "IO Exception detected: ", ioe);
794     connection.close();
795     if (connection == this.connection) {
796       this.connection = null; // To prevent close() from returning the connection to the pool.
797       close(false);
798     }
799     return new MessagingException(MessagingException.IOERROR, "IO Error", ioe);
800   }
801 
createMessage(String uid)802   public Message createMessage(String uid) {
803     return new ImapMessage(uid, this);
804   }
805 }
806