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