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    }
96
97    private void destroyResponses() {
98        if (mConnection != null) {
99            mConnection.destroyResponses();
100        }
101    }
102
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
154    public boolean isOpen() {
155        return mExists && mConnection != null;
156    }
157
158    public String getMode() {
159        return mMode;
160    }
161
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
177    public int getMessageCount() {
178        return mMessageCount;
179    }
180
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
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
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
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
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
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
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
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     */
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
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     */
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     */
487    private void handleUntaggedResponse(ImapResponse response) {
488        if (response.isDataResponse(1, ImapConstants.EXISTS)) {
489            mMessageCount = response.getStringOrEmpty(0).getNumberOrZero();
490        }
491    }
492
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
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
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     */
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
748    private void checkOpen() throws MessagingException {
749        if (!isOpen()) {
750            throw new MessagingException("Folder " + mName + " is not open.");
751        }
752    }
753
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
764    public Message createMessage(String uid) {
765        return new ImapMessage(uid, this);
766    }
767}