ImapFolder.java revision 9a01353f14f718c5447d6a3dc25b413be0f175d9
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.email.mail.store;
18
19import android.content.Context;
20import android.text.TextUtils;
21import android.util.Base64DataException;
22import android.util.Log;
23
24import com.android.email.Email;
25import com.android.email.mail.store.ImapStore.ImapException;
26import com.android.email.mail.store.ImapStore.ImapMessage;
27import com.android.email.mail.store.imap.ImapConstants;
28import com.android.email.mail.store.imap.ImapElement;
29import com.android.email.mail.store.imap.ImapList;
30import com.android.email.mail.store.imap.ImapResponse;
31import com.android.email.mail.store.imap.ImapString;
32import com.android.email.mail.store.imap.ImapUtility;
33import com.android.email.mail.transport.CountingOutputStream;
34import com.android.email.mail.transport.EOLConvertingOutputStream;
35import com.android.emailcommon.Logging;
36import com.android.emailcommon.internet.BinaryTempFileBody;
37import com.android.emailcommon.internet.MimeBodyPart;
38import com.android.emailcommon.internet.MimeHeader;
39import com.android.emailcommon.internet.MimeMultipart;
40import com.android.emailcommon.internet.MimeUtility;
41import com.android.emailcommon.mail.AuthenticationFailedException;
42import com.android.emailcommon.mail.Body;
43import com.android.emailcommon.mail.FetchProfile;
44import com.android.emailcommon.mail.Flag;
45import com.android.emailcommon.mail.Folder;
46import com.android.emailcommon.mail.Message;
47import com.android.emailcommon.mail.MessagingException;
48import com.android.emailcommon.mail.Part;
49import com.android.emailcommon.provider.Mailbox;
50import com.android.emailcommon.service.SearchParams;
51import com.android.emailcommon.utility.Utility;
52
53import java.io.IOException;
54import java.io.InputStream;
55import java.io.OutputStream;
56import java.util.ArrayList;
57import java.util.Arrays;
58import java.util.Date;
59import java.util.HashMap;
60import java.util.LinkedHashSet;
61import java.util.List;
62
63class ImapFolder extends Folder {
64    private final static Flag[] PERMANENT_FLAGS = { Flag.DELETED, Flag.SEEN, Flag.FLAGGED };
65    private static final int COPY_BUFFER_SIZE = 16*1024;
66
67    private final ImapStore mStore;
68    private final String mName;
69    private int mMessageCount = -1;
70    private ImapConnection mConnection;
71    private OpenMode mMode;
72    private boolean mExists;
73    /** The local mailbox associated with this remote folder */
74    Mailbox mMailbox;
75    /** A set of hashes that can be used to track dirtiness */
76    Object mHash[];
77
78    /*package*/ ImapFolder(ImapStore store, String name) {
79        mStore = store;
80        mName = name;
81    }
82
83    private void destroyResponses() {
84        if (mConnection != null) {
85            mConnection.destroyResponses();
86        }
87    }
88
89    @Override
90    public void open(OpenMode mode, PersistentDataCallbacks callbacks)
91            throws MessagingException {
92        try {
93            if (isOpenForTest()) {
94                if (mMode == mode) {
95                    // Make sure the connection is valid.
96                    // If it's not we'll close it down and continue on to get a new one.
97                    try {
98                        mConnection.executeSimpleCommand(ImapConstants.NOOP);
99                        return;
100
101                    } catch (IOException ioe) {
102                        ioExceptionHandler(mConnection, ioe);
103                    } finally {
104                        destroyResponses();
105                    }
106                } else {
107                    // Return the connection to the pool, if exists.
108                    close(false);
109                }
110            }
111            synchronized (this) {
112                mConnection = mStore.getConnection();
113            }
114            // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft NonJunk
115            // $MDNSent)
116            // * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft
117            // NonJunk $MDNSent \*)] Flags permitted.
118            // * 23 EXISTS
119            // * 0 RECENT
120            // * OK [UIDVALIDITY 1125022061] UIDs valid
121            // * OK [UIDNEXT 57576] Predicted next UID
122            // 2 OK [READ-WRITE] Select completed.
123            try {
124                doSelect();
125            } catch (IOException ioe) {
126                throw ioExceptionHandler(mConnection, ioe);
127            } finally {
128                destroyResponses();
129            }
130        } catch (AuthenticationFailedException e) {
131            // Don't cache this connection, so we're forced to try connecting/login again
132            mConnection = null;
133            close(false);
134            throw e;
135        } catch (MessagingException e) {
136            mExists = false;
137            close(false);
138            throw e;
139        }
140    }
141
142    @Override
143    public boolean isOpenForTest() {
144        return mExists && mConnection != null;
145    }
146
147    @Override
148    public OpenMode getMode() {
149        return mMode;
150    }
151
152    @Override
153    public void close(boolean expunge) {
154        // TODO implement expunge
155        mMessageCount = -1;
156        synchronized (this) {
157            mStore.poolConnection(mConnection);
158            mConnection = null;
159        }
160    }
161
162    @Override
163    public String getName() {
164        return mName;
165    }
166
167    @Override
168    public boolean exists() throws MessagingException {
169        if (mExists) {
170            return true;
171        }
172        /*
173         * This method needs to operate in the unselected mode as well as the selected mode
174         * so we must get the connection ourselves if it's not there. We are specifically
175         * not calling checkOpen() since we don't care if the folder is open.
176         */
177        ImapConnection connection = null;
178        synchronized(this) {
179            if (mConnection == null) {
180                connection = mStore.getConnection();
181            } else {
182                connection = mConnection;
183            }
184        }
185        try {
186            connection.executeSimpleCommand(String.format(
187                    ImapConstants.STATUS + " \"%s\" (" + ImapConstants.UIDVALIDITY + ")",
188                    ImapStore.encodeFolderName(mName, mStore.mPathPrefix)));
189            mExists = true;
190            return true;
191
192        } catch (MessagingException me) {
193            return false;
194
195        } catch (IOException ioe) {
196            throw ioExceptionHandler(connection, ioe);
197
198        } finally {
199            connection.destroyResponses();
200            if (mConnection == null) {
201                mStore.poolConnection(connection);
202            }
203        }
204    }
205
206    // IMAP supports folder creation
207    @Override
208    public boolean canCreate(FolderType type) {
209        return true;
210    }
211
212    @Override
213    public boolean create(FolderType type) throws MessagingException {
214        /*
215         * This method needs to operate in the unselected mode as well as the selected mode
216         * so we must get the connection ourselves if it's not there. We are specifically
217         * not calling checkOpen() since we don't care if the folder is open.
218         */
219        ImapConnection connection = null;
220        synchronized(this) {
221            if (mConnection == null) {
222                connection = mStore.getConnection();
223            } else {
224                connection = mConnection;
225            }
226        }
227        try {
228            connection.executeSimpleCommand(String.format(ImapConstants.CREATE + " \"%s\"",
229                    ImapStore.encodeFolderName(mName, mStore.mPathPrefix)));
230            return true;
231
232        } catch (MessagingException me) {
233            return false;
234
235        } catch (IOException ioe) {
236            throw ioExceptionHandler(connection, ioe);
237
238        } finally {
239            connection.destroyResponses();
240            if (mConnection == null) {
241                mStore.poolConnection(connection);
242            }
243        }
244    }
245
246    @Override
247    public void copyMessages(Message[] messages, Folder folder,
248            MessageUpdateCallbacks callbacks) throws MessagingException {
249        checkOpen();
250        try {
251            List<ImapResponse> responseList = mConnection.executeSimpleCommand(
252                    String.format(ImapConstants.UID_COPY + " %s \"%s\"",
253                            ImapStore.joinMessageUids(messages),
254                            ImapStore.encodeFolderName(folder.getName(), mStore.mPathPrefix)));
255            // Build a message map for faster UID matching
256            HashMap<String, Message> messageMap = new HashMap<String, Message>();
257            boolean handledUidPlus = false;
258            for (Message m : messages) {
259                messageMap.put(m.getUid(), m);
260            }
261            // Process response to get the new UIDs
262            for (ImapResponse response : responseList) {
263                // All "BAD" responses are bad. Only "NO", tagged responses are bad.
264                if (response.isBad() || (response.isNo() && response.isTagged())) {
265                    String responseText = response.getStatusResponseTextOrEmpty().getString();
266                    throw new MessagingException(responseText);
267                }
268                // Skip untagged responses; they're just status
269                if (!response.isTagged()) {
270                    continue;
271                }
272                // No callback provided to report of UID changes; nothing more to do here
273                // NOTE: We check this here to catch any server errors
274                if (callbacks == null) {
275                    continue;
276                }
277                ImapList copyResponse = response.getListOrEmpty(1);
278                String responseCode = copyResponse.getStringOrEmpty(0).getString();
279                if (ImapConstants.COPYUID.equals(responseCode)) {
280                    handledUidPlus = true;
281                    String origIdSet = copyResponse.getStringOrEmpty(2).getString();
282                    String newIdSet = copyResponse.getStringOrEmpty(3).getString();
283                    String[] origIdArray = ImapUtility.getImapSequenceValues(origIdSet);
284                    String[] newIdArray = ImapUtility.getImapSequenceValues(newIdSet);
285                    // There has to be a 1:1 mapping between old and new IDs
286                    if (origIdArray.length != newIdArray.length) {
287                        throw new MessagingException("Set length mis-match; orig IDs \"" +
288                                origIdSet + "\"  new IDs \"" + newIdSet + "\"");
289                    }
290                    for (int i = 0; i < origIdArray.length; i++) {
291                        final String id = origIdArray[i];
292                        final Message m = messageMap.get(id);
293                        if (m != null) {
294                            callbacks.onMessageUidChange(m, newIdArray[i]);
295                        }
296                    }
297                }
298            }
299            // If the server doesn't support UIDPLUS, try a different way to get the new UID(s)
300            if (callbacks != null && !handledUidPlus) {
301                ImapFolder newFolder = (ImapFolder)folder;
302                try {
303                    // Temporarily select the destination folder
304                    newFolder.open(OpenMode.READ_WRITE, null);
305                    // Do the search(es) ...
306                    for (Message m : messages) {
307                        String searchString = "HEADER Message-Id \"" + m.getMessageId() + "\"";
308                        String[] newIdArray = newFolder.searchForUids(searchString);
309                        if (newIdArray.length == 1) {
310                            callbacks.onMessageUidChange(m, newIdArray[0]);
311                        }
312                    }
313                } catch (MessagingException e) {
314                    // Log, but, don't abort; failures here don't need to be propagated
315                    Log.d(Logging.LOG_TAG, "Failed to find message", e);
316                } finally {
317                    newFolder.close(false);
318                }
319                // Re-select the original folder
320                doSelect();
321            }
322        } catch (IOException ioe) {
323            throw ioExceptionHandler(mConnection, ioe);
324        } finally {
325            destroyResponses();
326        }
327    }
328
329    @Override
330    public int getMessageCount() {
331        return mMessageCount;
332    }
333
334    @Override
335    public int getUnreadMessageCount() throws MessagingException {
336        checkOpen();
337        try {
338            int unreadMessageCount = 0;
339            List<ImapResponse> responses = mConnection.executeSimpleCommand(String.format(
340                    ImapConstants.STATUS + " \"%s\" (" + ImapConstants.UNSEEN + ")",
341                    ImapStore.encodeFolderName(mName, mStore.mPathPrefix)));
342            // S: * STATUS mboxname (MESSAGES 231 UIDNEXT 44292)
343            for (ImapResponse response : responses) {
344                if (response.isDataResponse(0, ImapConstants.STATUS)) {
345                    unreadMessageCount = response.getListOrEmpty(2)
346                            .getKeyedStringOrEmpty(ImapConstants.UNSEEN).getNumberOrZero();
347                }
348            }
349            return unreadMessageCount;
350        } catch (IOException ioe) {
351            throw ioExceptionHandler(mConnection, ioe);
352        } finally {
353            destroyResponses();
354        }
355    }
356
357    @Override
358    public void delete(boolean recurse) {
359        throw new Error("ImapStore.delete() not yet implemented");
360    }
361
362    /* package */ String[] searchForUids(String searchCriteria)
363            throws MessagingException {
364        checkOpen();
365        List<ImapResponse> responses;
366        try {
367            try {
368                responses = mConnection.executeSimpleCommand(
369                        ImapConstants.UID_SEARCH + " " + searchCriteria);
370            } catch (ImapException e) {
371                return Utility.EMPTY_STRINGS; // not found;
372            } catch (IOException ioe) {
373                throw ioExceptionHandler(mConnection, ioe);
374            }
375            // S: * SEARCH 2 3 6
376            final ArrayList<String> uids = new ArrayList<String>();
377            for (ImapResponse response : responses) {
378                if (!response.isDataResponse(0, ImapConstants.SEARCH)) {
379                    continue;
380                }
381                // Found SEARCH response data
382                for (int i = 1; i < response.size(); i++) {
383                    ImapString s = response.getStringOrEmpty(i);
384                    if (s.isString()) {
385                        uids.add(s.getString());
386                    }
387                }
388            }
389            return uids.toArray(Utility.EMPTY_STRINGS);
390        } finally {
391            destroyResponses();
392        }
393    }
394
395    @Override
396    public Message getMessage(String uid) throws MessagingException {
397        checkOpen();
398
399        String[] uids = searchForUids(ImapConstants.UID + " " + uid);
400        for (int i = 0; i < uids.length; i++) {
401            if (uids[i].equals(uid)) {
402                return new ImapMessage(uid, this);
403            }
404        }
405        return null;
406    }
407
408    /**
409     * Retrieve messages based on search parameters.  We search FROM, TO, CC, SUBJECT, and BODY
410     * We send: SEARCH OR FROM "foo" (OR TO "foo" (OR CC "foo" (OR SUBJECT "foo" BODY "foo")))
411     * TODO: Properly quote the filter
412     */
413    @Override
414    public Message[] getMessages(SearchParams params, MessageRetrievalListener listener)
415            throws MessagingException {
416        String filter = params.mFilter;
417        StringBuilder sb = new StringBuilder();
418        sb.append("OR FROM \"");
419        sb.append(filter);
420        sb.append("\" (OR TO \"");
421        sb.append(filter);
422        sb.append("\" (OR CC \"");
423        sb.append(filter);
424        sb.append("\" (OR SUBJECT \"");
425        sb.append(filter);
426        sb.append("\" BODY \"");
427        sb.append(filter);
428        sb.append("\")))");
429        return getMessagesInternal(searchForUids(sb.toString()), listener);
430    }
431
432    @Override
433    public Message[] getMessages(int start, int end, MessageRetrievalListener listener)
434            throws MessagingException {
435        if (start < 1 || end < 1 || end < start) {
436            throw new MessagingException(String.format("Invalid range: %d %d", start, end));
437        }
438        return getMessagesInternal(
439                searchForUids(String.format("%d:%d NOT DELETED", start, end)), listener);
440    }
441
442    @Override
443    public Message[] getMessages(String[] uids, MessageRetrievalListener listener)
444            throws MessagingException {
445        if (uids == null) {
446            uids = searchForUids("1:* NOT DELETED");
447        }
448        return getMessagesInternal(uids, listener);
449    }
450
451    public Message[] getMessagesInternal(String[] uids, MessageRetrievalListener listener) {
452        final ArrayList<Message> messages = new ArrayList<Message>(uids.length);
453        for (int i = 0; i < uids.length; i++) {
454            final String uid = uids[i];
455            final ImapMessage message = new ImapMessage(uid, this);
456            messages.add(message);
457            if (listener != null) {
458                listener.messageRetrieved(message);
459            }
460        }
461        return messages.toArray(Message.EMPTY_ARRAY);
462    }
463
464    @Override
465    public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)
466            throws MessagingException {
467        try {
468            fetchInternal(messages, fp, listener);
469        } catch (RuntimeException e) { // Probably a parser error.
470            Log.w(Logging.LOG_TAG, "Exception detected: " + e.getMessage());
471            if (mConnection != null) {
472                mConnection.logLastDiscourse();
473            }
474            throw e;
475        }
476    }
477
478    public void fetchInternal(Message[] messages, FetchProfile fp,
479            MessageRetrievalListener listener) throws MessagingException {
480        if (messages.length == 0) {
481            return;
482        }
483        checkOpen();
484        HashMap<String, Message> messageMap = new HashMap<String, Message>();
485        for (Message m : messages) {
486            messageMap.put(m.getUid(), m);
487        }
488
489        /*
490         * Figure out what command we are going to run:
491         * FLAGS     - UID FETCH (FLAGS)
492         * ENVELOPE  - UID FETCH (INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[
493         *                            HEADER.FIELDS (date subject from content-type to cc)])
494         * STRUCTURE - UID FETCH (BODYSTRUCTURE)
495         * BODY_SANE - UID FETCH (BODY.PEEK[]<0.N>) where N = max bytes returned
496         * BODY      - UID FETCH (BODY.PEEK[])
497         * Part      - UID FETCH (BODY.PEEK[ID]) where ID = mime part ID
498         */
499
500        final LinkedHashSet<String> fetchFields = new LinkedHashSet<String>();
501
502        fetchFields.add(ImapConstants.UID);
503        if (fp.contains(FetchProfile.Item.FLAGS)) {
504            fetchFields.add(ImapConstants.FLAGS);
505        }
506        if (fp.contains(FetchProfile.Item.ENVELOPE)) {
507            fetchFields.add(ImapConstants.INTERNALDATE);
508            fetchFields.add(ImapConstants.RFC822_SIZE);
509            fetchFields.add(ImapConstants.FETCH_FIELD_HEADERS);
510        }
511        if (fp.contains(FetchProfile.Item.STRUCTURE)) {
512            fetchFields.add(ImapConstants.BODYSTRUCTURE);
513        }
514
515        if (fp.contains(FetchProfile.Item.BODY_SANE)) {
516            fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_SANE);
517        }
518        if (fp.contains(FetchProfile.Item.BODY)) {
519            fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK);
520        }
521
522        final Part fetchPart = fp.getFirstPart();
523        if (fetchPart != null) {
524            String[] partIds =
525                    fetchPart.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA);
526            if (partIds != null) {
527                fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_BARE
528                        + "[" + partIds[0] + "]");
529            }
530        }
531
532        try {
533            mConnection.sendCommand(String.format(
534                    ImapConstants.UID_FETCH + " %s (%s)", ImapStore.joinMessageUids(messages),
535                    Utility.combine(fetchFields.toArray(new String[fetchFields.size()]), ' ')
536                    ), false);
537            ImapResponse response;
538            int messageNumber = 0;
539            do {
540                response = null;
541                try {
542                    response = mConnection.readResponse();
543
544                    if (!response.isDataResponse(1, ImapConstants.FETCH)) {
545                        continue; // Ignore
546                    }
547                    final ImapList fetchList = response.getListOrEmpty(2);
548                    final String uid = fetchList.getKeyedStringOrEmpty(ImapConstants.UID)
549                            .getString();
550                    if (TextUtils.isEmpty(uid)) continue;
551
552                    ImapMessage message = (ImapMessage) messageMap.get(uid);
553                    if (message == null) continue;
554
555                    if (fp.contains(FetchProfile.Item.FLAGS)) {
556                        final ImapList flags =
557                            fetchList.getKeyedListOrEmpty(ImapConstants.FLAGS);
558                        for (int i = 0, count = flags.size(); i < count; i++) {
559                            final ImapString flag = flags.getStringOrEmpty(i);
560                            if (flag.is(ImapConstants.FLAG_DELETED)) {
561                                message.setFlagInternal(Flag.DELETED, true);
562                            } else if (flag.is(ImapConstants.FLAG_ANSWERED)) {
563                                message.setFlagInternal(Flag.ANSWERED, true);
564                            } else if (flag.is(ImapConstants.FLAG_SEEN)) {
565                                message.setFlagInternal(Flag.SEEN, true);
566                            } else if (flag.is(ImapConstants.FLAG_FLAGGED)) {
567                                message.setFlagInternal(Flag.FLAGGED, true);
568                            }
569                        }
570                    }
571                    if (fp.contains(FetchProfile.Item.ENVELOPE)) {
572                        final Date internalDate = fetchList.getKeyedStringOrEmpty(
573                                ImapConstants.INTERNALDATE).getDateOrNull();
574                        final int size = fetchList.getKeyedStringOrEmpty(
575                                ImapConstants.RFC822_SIZE).getNumberOrZero();
576                        final String header = fetchList.getKeyedStringOrEmpty(
577                                ImapConstants.BODY_BRACKET_HEADER, true).getString();
578
579                        message.setInternalDate(internalDate);
580                        message.setSize(size);
581                        message.parse(Utility.streamFromAsciiString(header));
582                    }
583                    if (fp.contains(FetchProfile.Item.STRUCTURE)) {
584                        ImapList bs = fetchList.getKeyedListOrEmpty(
585                                ImapConstants.BODYSTRUCTURE);
586                        if (!bs.isEmpty()) {
587                            try {
588                                parseBodyStructure(bs, message, ImapConstants.TEXT);
589                            } catch (MessagingException e) {
590                                if (Logging.LOGD) {
591                                    Log.v(Logging.LOG_TAG, "Error handling message", e);
592                                }
593                                message.setBody(null);
594                            }
595                        }
596                    }
597                    if (fp.contains(FetchProfile.Item.BODY)
598                            || fp.contains(FetchProfile.Item.BODY_SANE)) {
599                        // Body is keyed by "BODY[]...".
600                        // Previously used "BODY[..." but this can be confused with "BODY[HEADER..."
601                        // TODO Should we accept "RFC822" as well??
602                        ImapString body = fetchList.getKeyedStringOrEmpty("BODY[]", true);
603                        String bodyText = body.getString();
604                        InputStream bodyStream = body.getAsStream();
605                        message.parse(bodyStream);
606                    }
607                    if (fetchPart != null && fetchPart.getSize() > 0) {
608                        InputStream bodyStream =
609                                fetchList.getKeyedStringOrEmpty("BODY[", true).getAsStream();
610                        String contentType = fetchPart.getContentType();
611                        String contentTransferEncoding = fetchPart.getHeader(
612                                MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING)[0];
613
614                        // TODO Don't create 2 temp files.
615                        // decodeBody creates BinaryTempFileBody, but we could avoid this
616                        // if we implement ImapStringBody.
617                        // (We'll need to share a temp file.  Protect it with a ref-count.)
618                        fetchPart.setBody(decodeBody(bodyStream, contentTransferEncoding,
619                                fetchPart.getSize(), listener));
620                    }
621
622                    if (listener != null) {
623                        listener.messageRetrieved(message);
624                    }
625                } finally {
626                    destroyResponses();
627                }
628            } while (!response.isTagged());
629        } catch (IOException ioe) {
630            throw ioExceptionHandler(mConnection, ioe);
631        }
632    }
633
634    /**
635     * Removes any content transfer encoding from the stream and returns a Body.
636     * This code is taken/condensed from MimeUtility.decodeBody
637     */
638    private Body decodeBody(InputStream in, String contentTransferEncoding, int size,
639            MessageRetrievalListener listener) throws IOException {
640        // Get a properly wrapped input stream
641        in = MimeUtility.getInputStreamForContentTransferEncoding(in, contentTransferEncoding);
642        BinaryTempFileBody tempBody = new BinaryTempFileBody();
643        OutputStream out = tempBody.getOutputStream();
644        try {
645            byte[] buffer = new byte[COPY_BUFFER_SIZE];
646            int n = 0;
647            int count = 0;
648            while (-1 != (n = in.read(buffer))) {
649                out.write(buffer, 0, n);
650                count += n;
651                if (listener != null) {
652                    listener.loadAttachmentProgress(count * 100 / size);
653                }
654            }
655        } catch (Base64DataException bde) {
656            String warning = "\n\n" + Email.getMessageDecodeErrorString();
657            out.write(warning.getBytes());
658        } finally {
659            out.close();
660        }
661        return tempBody;
662    }
663
664    @Override
665    public Flag[] getPermanentFlags() {
666        return PERMANENT_FLAGS;
667    }
668
669    /**
670     * Handle any untagged responses that the caller doesn't care to handle themselves.
671     * @param responses
672     */
673    private void handleUntaggedResponses(List<ImapResponse> responses) {
674        for (ImapResponse response : responses) {
675            handleUntaggedResponse(response);
676        }
677    }
678
679    /**
680     * Handle an untagged response that the caller doesn't care to handle themselves.
681     * @param response
682     */
683    private void handleUntaggedResponse(ImapResponse response) {
684        if (response.isDataResponse(1, ImapConstants.EXISTS)) {
685            mMessageCount = response.getStringOrEmpty(0).getNumberOrZero();
686        }
687    }
688
689    private static void parseBodyStructure(ImapList bs, Part part, String id)
690            throws MessagingException {
691        if (bs.getElementOrNone(0).isList()) {
692            /*
693             * This is a multipart/*
694             */
695            MimeMultipart mp = new MimeMultipart();
696            for (int i = 0, count = bs.size(); i < count; i++) {
697                ImapElement e = bs.getElementOrNone(i);
698                if (e.isList()) {
699                    /*
700                     * For each part in the message we're going to add a new BodyPart and parse
701                     * into it.
702                     */
703                    MimeBodyPart bp = new MimeBodyPart();
704                    if (id.equals(ImapConstants.TEXT)) {
705                        parseBodyStructure(bs.getListOrEmpty(i), bp, Integer.toString(i + 1));
706
707                    } else {
708                        parseBodyStructure(bs.getListOrEmpty(i), bp, id + "." + (i + 1));
709                    }
710                    mp.addBodyPart(bp);
711
712                } else {
713                    if (e.isString()) {
714                        mp.setSubType(bs.getStringOrEmpty(i).getString().toLowerCase());
715                    }
716                    break; // Ignore the rest of the list.
717                }
718            }
719            part.setBody(mp);
720        } else {
721            /*
722             * This is a body. We need to add as much information as we can find out about
723             * it to the Part.
724             */
725
726            /*
727             body type
728             body subtype
729             body parameter parenthesized list
730             body id
731             body description
732             body encoding
733             body size
734             */
735
736            final ImapString type = bs.getStringOrEmpty(0);
737            final ImapString subType = bs.getStringOrEmpty(1);
738            final String mimeType =
739                    (type.getString() + "/" + subType.getString()).toLowerCase();
740
741            final ImapList bodyParams = bs.getListOrEmpty(2);
742            final ImapString cid = bs.getStringOrEmpty(3);
743            final ImapString encoding = bs.getStringOrEmpty(5);
744            final int size = bs.getStringOrEmpty(6).getNumberOrZero();
745
746            if (MimeUtility.mimeTypeMatches(mimeType, MimeUtility.MIME_TYPE_RFC822)) {
747                // A body type of type MESSAGE and subtype RFC822
748                // contains, immediately after the basic fields, the
749                // envelope structure, body structure, and size in
750                // text lines of the encapsulated message.
751                // [MESSAGE, RFC822, [NAME, filename.eml], NIL, NIL, 7BIT, 5974, NIL,
752                //     [INLINE, [FILENAME*0, Fwd: Xxx..., FILENAME*1, filename.eml]], NIL]
753                /*
754                 * This will be caught by fetch and handled appropriately.
755                 */
756                throw new MessagingException("BODYSTRUCTURE " + MimeUtility.MIME_TYPE_RFC822
757                        + " not yet supported.");
758            }
759
760            /*
761             * Set the content type with as much information as we know right now.
762             */
763            final StringBuilder contentType = new StringBuilder(mimeType);
764
765            /*
766             * If there are body params we might be able to get some more information out
767             * of them.
768             */
769            for (int i = 1, count = bodyParams.size(); i < count; i += 2) {
770
771                // TODO We need to convert " into %22, but
772                // because MimeUtility.getHeaderParameter doesn't recognize it,
773                // we can't fix it for now.
774                contentType.append(String.format(";\n %s=\"%s\"",
775                        bodyParams.getStringOrEmpty(i - 1).getString(),
776                        bodyParams.getStringOrEmpty(i).getString()));
777            }
778
779            part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType.toString());
780
781            // Extension items
782            final ImapList bodyDisposition;
783
784            if (type.is(ImapConstants.TEXT) && bs.getElementOrNone(9).isList()) {
785                // If media-type is TEXT, 9th element might be: [body-fld-lines] := number
786                // So, if it's not a list, use 10th element.
787                // (Couldn't find evidence in the RFC if it's ALWAYS 10th element.)
788                bodyDisposition = bs.getListOrEmpty(9);
789            } else {
790                bodyDisposition = bs.getListOrEmpty(8);
791            }
792
793            final StringBuilder contentDisposition = new StringBuilder();
794
795            if (bodyDisposition.size() > 0) {
796                final String bodyDisposition0Str =
797                        bodyDisposition.getStringOrEmpty(0).getString().toLowerCase();
798                if (!TextUtils.isEmpty(bodyDisposition0Str)) {
799                    contentDisposition.append(bodyDisposition0Str);
800                }
801
802                final ImapList bodyDispositionParams = bodyDisposition.getListOrEmpty(1);
803                if (!bodyDispositionParams.isEmpty()) {
804                    /*
805                     * If there is body disposition information we can pull some more
806                     * information about the attachment out.
807                     */
808                    for (int i = 1, count = bodyDispositionParams.size(); i < count; i += 2) {
809
810                        // TODO We need to convert " into %22.  See above.
811                        contentDisposition.append(String.format(";\n %s=\"%s\"",
812                                bodyDispositionParams.getStringOrEmpty(i - 1)
813                                        .getString().toLowerCase(),
814                                bodyDispositionParams.getStringOrEmpty(i).getString()));
815                    }
816                }
817            }
818
819            if ((size > 0)
820                    && (MimeUtility.getHeaderParameter(contentDisposition.toString(), "size")
821                            == null)) {
822                contentDisposition.append(String.format(";\n size=%d", size));
823            }
824
825            if (contentDisposition.length() > 0) {
826                /*
827                 * Set the content disposition containing at least the size. Attachment
828                 * handling code will use this down the road.
829                 */
830                part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION,
831                        contentDisposition.toString());
832            }
833
834            /*
835             * Set the Content-Transfer-Encoding header. Attachment code will use this
836             * to parse the body.
837             */
838            if (!encoding.isEmpty()) {
839                part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING,
840                        encoding.getString());
841            }
842
843            /*
844             * Set the Content-ID header.
845             */
846            if (!cid.isEmpty()) {
847                part.setHeader(MimeHeader.HEADER_CONTENT_ID, cid.getString());
848            }
849
850            if (size > 0) {
851                if (part instanceof ImapMessage) {
852                    ((ImapMessage) part).setSize(size);
853                } else if (part instanceof MimeBodyPart) {
854                    ((MimeBodyPart) part).setSize(size);
855                } else {
856                    throw new MessagingException("Unknown part type " + part.toString());
857                }
858            }
859            part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id);
860        }
861
862    }
863
864    /**
865     * Appends the given messages to the selected folder. This implementation also determines
866     * the new UID of the given message on the IMAP server and sets the Message's UID to the
867     * new server UID.
868     */
869    @Override
870    public void appendMessages(Message[] messages) throws MessagingException {
871        checkOpen();
872        try {
873            for (Message message : messages) {
874                // Create output count
875                CountingOutputStream out = new CountingOutputStream();
876                EOLConvertingOutputStream eolOut = new EOLConvertingOutputStream(out);
877                message.writeTo(eolOut);
878                eolOut.flush();
879                // Create flag list (most often this will be "\SEEN")
880                String flagList = "";
881                Flag[] flags = message.getFlags();
882                if (flags.length > 0) {
883                    StringBuilder sb = new StringBuilder();
884                    for (int i = 0, count = flags.length; i < count; i++) {
885                        Flag flag = flags[i];
886                        if (flag == Flag.SEEN) {
887                            sb.append(" " + ImapConstants.FLAG_SEEN);
888                        } else if (flag == Flag.FLAGGED) {
889                            sb.append(" " + ImapConstants.FLAG_FLAGGED);
890                        }
891                    }
892                    if (sb.length() > 0) {
893                        flagList = sb.substring(1);
894                    }
895                }
896
897                mConnection.sendCommand(
898                        String.format(ImapConstants.APPEND + " \"%s\" (%s) {%d}",
899                                ImapStore.encodeFolderName(mName, mStore.mPathPrefix),
900                                flagList,
901                                out.getCount()), false);
902                ImapResponse response;
903                do {
904                    response = mConnection.readResponse();
905                    if (response.isContinuationRequest()) {
906                        eolOut = new EOLConvertingOutputStream(
907                                mConnection.mTransport.getOutputStream());
908                        message.writeTo(eolOut);
909                        eolOut.write('\r');
910                        eolOut.write('\n');
911                        eolOut.flush();
912                    } else if (!response.isTagged()) {
913                        handleUntaggedResponse(response);
914                    }
915                } while (!response.isTagged());
916
917                // TODO Why not check the response?
918
919                /*
920                 * Try to recover the UID of the message from an APPENDUID response.
921                 * e.g. 11 OK [APPENDUID 2 238268] APPEND completed
922                 */
923                final ImapList appendList = response.getListOrEmpty(1);
924                if ((appendList.size() >= 3) && appendList.is(0, ImapConstants.APPENDUID)) {
925                    String serverUid = appendList.getStringOrEmpty(2).getString();
926                    if (!TextUtils.isEmpty(serverUid)) {
927                        message.setUid(serverUid);
928                        continue;
929                    }
930                }
931
932                /*
933                 * Try to find the UID of the message we just appended using the
934                 * Message-ID header.  If there are more than one response, take the
935                 * last one, as it's most likely the newest (the one we just uploaded).
936                 */
937                String messageId = message.getMessageId();
938                if (messageId == null || messageId.length() == 0) {
939                    continue;
940                }
941                // Most servers don't care about parenthesis in the search query [and, some
942                // fail to work if they are used]
943                String[] uids = searchForUids(String.format("HEADER MESSAGE-ID %s", messageId));
944                if (uids.length > 0) {
945                    message.setUid(uids[0]);
946                }
947                // However, there's at least one server [AOL] that fails to work unless there
948                // are parenthesis, so, try this as a last resort
949                uids = searchForUids(String.format("(HEADER MESSAGE-ID %s)", messageId));
950                if (uids.length > 0) {
951                    message.setUid(uids[0]);
952                }
953            }
954        } catch (IOException ioe) {
955            throw ioExceptionHandler(mConnection, ioe);
956        } finally {
957            destroyResponses();
958        }
959    }
960
961    @Override
962    public Message[] expunge() throws MessagingException {
963        checkOpen();
964        try {
965            handleUntaggedResponses(mConnection.executeSimpleCommand(ImapConstants.EXPUNGE));
966        } catch (IOException ioe) {
967            throw ioExceptionHandler(mConnection, ioe);
968        } finally {
969            destroyResponses();
970        }
971        return null;
972    }
973
974    @Override
975    public void setFlags(Message[] messages, Flag[] flags, boolean value)
976            throws MessagingException {
977        checkOpen();
978
979        String allFlags = "";
980        if (flags.length > 0) {
981            StringBuilder flagList = new StringBuilder();
982            for (int i = 0, count = flags.length; i < count; i++) {
983                Flag flag = flags[i];
984                if (flag == Flag.SEEN) {
985                    flagList.append(" " + ImapConstants.FLAG_SEEN);
986                } else if (flag == Flag.DELETED) {
987                    flagList.append(" " + ImapConstants.FLAG_DELETED);
988                } else if (flag == Flag.FLAGGED) {
989                    flagList.append(" " + ImapConstants.FLAG_FLAGGED);
990                }
991            }
992            allFlags = flagList.substring(1);
993        }
994        try {
995            mConnection.executeSimpleCommand(String.format(
996                    ImapConstants.UID_STORE + " %s %s" + ImapConstants.FLAGS_SILENT + " (%s)",
997                    ImapStore.joinMessageUids(messages),
998                    value ? "+" : "-",
999                    allFlags));
1000
1001        } catch (IOException ioe) {
1002            throw ioExceptionHandler(mConnection, ioe);
1003        } finally {
1004            destroyResponses();
1005        }
1006    }
1007
1008    /**
1009     * Persists this folder. We will always perform the proper database operation (e.g.
1010     * 'save' or 'update'). As an optimization, if a folder has not been modified, no
1011     * database operations are performed.
1012     */
1013    void save(Context context) {
1014        final Mailbox mailbox = mMailbox;
1015        if (!mailbox.isSaved()) {
1016            mailbox.save(context);
1017            mHash = mailbox.getHashes();
1018        } else {
1019            Object[] hash = mailbox.getHashes();
1020            if (!Arrays.equals(mHash, hash)) {
1021                mailbox.update(context, mailbox.toContentValues());
1022                mHash = hash;  // Save updated hash
1023            }
1024        }
1025    }
1026
1027    /**
1028     * Selects the folder for use. Before performing any operations on this folder, it
1029     * must be selected.
1030     */
1031    private void doSelect() throws IOException, MessagingException {
1032        List<ImapResponse> responses = mConnection.executeSimpleCommand(
1033                String.format(ImapConstants.SELECT + " \"%s\"",
1034                        ImapStore.encodeFolderName(mName, mStore.mPathPrefix)));
1035
1036        // Assume the folder is opened read-write; unless we are notified otherwise
1037        mMode = OpenMode.READ_WRITE;
1038        int messageCount = -1;
1039        for (ImapResponse response : responses) {
1040            if (response.isDataResponse(1, ImapConstants.EXISTS)) {
1041                messageCount = response.getStringOrEmpty(0).getNumberOrZero();
1042            } else if (response.isOk()) {
1043                final ImapString responseCode = response.getResponseCodeOrEmpty();
1044                if (responseCode.is(ImapConstants.READ_ONLY)) {
1045                    mMode = OpenMode.READ_ONLY;
1046                } else if (responseCode.is(ImapConstants.READ_WRITE)) {
1047                    mMode = OpenMode.READ_WRITE;
1048                }
1049            } else if (response.isTagged()) { // Not OK
1050                throw new MessagingException("Can't open mailbox: "
1051                        + response.getStatusResponseTextOrEmpty());
1052            }
1053        }
1054        if (messageCount == -1) {
1055            throw new MessagingException("Did not find message count during select");
1056        }
1057        mMessageCount = messageCount;
1058        mExists = true;
1059    }
1060
1061    private void checkOpen() throws MessagingException {
1062        if (!isOpenForTest()) {
1063            throw new MessagingException("Folder " + mName + " is not open.");
1064        }
1065    }
1066
1067    private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) {
1068        if (Email.DEBUG) {
1069            Log.d(Logging.LOG_TAG, "IO Exception detected: ", ioe);
1070        }
1071        connection.close();
1072        if (connection == mConnection) {
1073            mConnection = null; // To prevent close() from returning the connection to the pool.
1074            close(false);
1075        }
1076        return new MessagingException("IO Error", ioe);
1077    }
1078
1079    @Override
1080    public boolean equals(Object o) {
1081        if (o instanceof ImapFolder) {
1082            return ((ImapFolder)o).mName.equals(mName);
1083        }
1084        return super.equals(o);
1085    }
1086
1087    @Override
1088    public Message createMessage(String uid) {
1089        return new ImapMessage(uid, this);
1090    }
1091}
1092