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