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