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