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