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