ImapStore.java revision 96c5af40d639d629267794f4f0338a267ff94ce5
1/*
2 * Copyright (C) 2008 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 com.android.email.Email;
20import com.android.email.PeekableInputStream;
21import com.android.email.Utility;
22import com.android.email.mail.AuthenticationFailedException;
23import com.android.email.mail.CertificateValidationException;
24import com.android.email.mail.FetchProfile;
25import com.android.email.mail.Flag;
26import com.android.email.mail.Folder;
27import com.android.email.mail.Message;
28import com.android.email.mail.MessageRetrievalListener;
29import com.android.email.mail.MessagingException;
30import com.android.email.mail.Part;
31import com.android.email.mail.Store;
32import com.android.email.mail.Transport;
33import com.android.email.mail.internet.MimeBodyPart;
34import com.android.email.mail.internet.MimeHeader;
35import com.android.email.mail.internet.MimeMessage;
36import com.android.email.mail.internet.MimeMultipart;
37import com.android.email.mail.internet.MimeUtility;
38import com.android.email.mail.store.ImapResponseParser.ImapList;
39import com.android.email.mail.store.ImapResponseParser.ImapResponse;
40import com.android.email.mail.transport.CountingOutputStream;
41import com.android.email.mail.transport.EOLConvertingOutputStream;
42import com.android.email.mail.transport.MailTransport;
43import com.beetstra.jutf7.CharsetProvider;
44
45import android.util.Config;
46import android.util.Log;
47
48import java.io.IOException;
49import java.io.InputStream;
50import java.io.UnsupportedEncodingException;
51import java.net.URI;
52import java.net.URISyntaxException;
53import java.nio.ByteBuffer;
54import java.nio.CharBuffer;
55import java.nio.charset.Charset;
56import java.util.ArrayList;
57import java.util.Date;
58import java.util.HashMap;
59import java.util.LinkedHashSet;
60import java.util.LinkedList;
61import java.util.List;
62
63import javax.net.ssl.SSLException;
64
65/**
66 * <pre>
67 * TODO Need to start keeping track of UIDVALIDITY
68 * TODO Need a default response handler for things like folder updates
69 * TODO In fetch(), if we need a ImapMessage and were given
70 * something else we can try to do a pre-fetch first.
71 *
72 * ftp://ftp.isi.edu/in-notes/rfc2683.txt When a client asks for
73 * certain information in a FETCH command, the server may return the requested
74 * information in any order, not necessarily in the order that it was requested.
75 * Further, the server may return the information in separate FETCH responses
76 * and may also return information that was not explicitly requested (to reflect
77 * to the client changes in the state of the subject message).
78 * </pre>
79 */
80public class ImapStore extends Store {
81    public static final int CONNECTION_SECURITY_NONE = 0;
82    public static final int CONNECTION_SECURITY_TLS_OPTIONAL = 1;
83    public static final int CONNECTION_SECURITY_TLS_REQUIRED = 2;
84    public static final int CONNECTION_SECURITY_SSL_REQUIRED = 3;
85    public static final int CONNECTION_SECURITY_SSL_OPTIONAL = 4;
86
87    private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED, Flag.SEEN };
88
89    private Transport mRootTransport;
90    private String mUsername;
91    private String mPassword;
92    private String mLoginPhrase;
93    private String mPathPrefix;
94
95    private LinkedList<ImapConnection> mConnections =
96            new LinkedList<ImapConnection>();
97
98    /**
99     * Charset used for converting folder names to and from UTF-7 as defined by RFC 3501.
100     */
101    private Charset mModifiedUtf7Charset;
102
103    /**
104     * Cache of ImapFolder objects. ImapFolders are attached to a given folder on the server
105     * and as long as their associated connection remains open they are reusable between
106     * requests. This cache lets us make sure we always reuse, if possible, for a given
107     * folder name.
108     */
109    private HashMap<String, ImapFolder> mFolderCache = new HashMap<String, ImapFolder>();
110
111    /**
112     * Allowed formats for the Uri:
113     * imap://user:password@server:port CONNECTION_SECURITY_NONE
114     * imap+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL
115     * imap+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED
116     * imap+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED
117     * imap+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL
118     *
119     * @param uriString the Uri containing information to configure this store
120     */
121    public ImapStore(String uriString) throws MessagingException {
122        URI uri;
123        try {
124            uri = new URI(uriString);
125        } catch (URISyntaxException use) {
126            throw new MessagingException("Invalid ImapStore URI", use);
127        }
128
129        String scheme = uri.getScheme();
130        int connectionSecurity = Transport.CONNECTION_SECURITY_NONE;
131        int defaultPort = -1;
132        if (scheme.equals("imap")) {
133            connectionSecurity = CONNECTION_SECURITY_NONE;
134            defaultPort = 143;
135        } else if (scheme.equals("imap+tls")) {
136            connectionSecurity = CONNECTION_SECURITY_TLS_OPTIONAL;
137            defaultPort = 143;
138        } else if (scheme.equals("imap+tls+")) {
139            connectionSecurity = CONNECTION_SECURITY_TLS_REQUIRED;
140            defaultPort = 143;
141        } else if (scheme.equals("imap+ssl+")) {
142            connectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED;
143            defaultPort = 993;
144        } else if (scheme.equals("imap+ssl")) {
145            connectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL;
146            defaultPort = 993;
147        } else {
148            throw new MessagingException("Unsupported protocol");
149        }
150
151        mRootTransport = new MailTransport("IMAP");
152        mRootTransport.setUri(uri, defaultPort);
153        mRootTransport.setSecurity(connectionSecurity);
154
155        String[] userInfoParts = mRootTransport.getUserInfoParts();
156        if (userInfoParts != null) {
157            mUsername = userInfoParts[0];
158            if (userInfoParts.length > 1) {
159                mPassword = userInfoParts[1];
160
161                // build the LOGIN string once (instead of over-and-over again.)
162                // apply the quoting here around the built-up password
163                mLoginPhrase = "LOGIN " + mUsername + " " + Utility.imapQuoted(mPassword);
164            }
165        }
166
167        if ((uri.getPath() != null) && (uri.getPath().length() > 0)) {
168            mPathPrefix = uri.getPath().substring(1);
169        }
170
171        mModifiedUtf7Charset = new CharsetProvider().charsetForName("X-RFC-3501");
172    }
173
174    /**
175     * For testing only.  Injects a different root transport (it will be copied using
176     * newInstanceWithConfiguration() each time IMAP sets up a new channel).  The transport
177     * should already be set up and ready to use.  Do not use for real code.
178     * @param testTransport The Transport to inject and use for all future communication.
179     */
180    /* package */ void setTransport(Transport testTransport) {
181        mRootTransport = testTransport;
182    }
183
184    @Override
185    public Folder getFolder(String name) throws MessagingException {
186        ImapFolder folder;
187        synchronized (mFolderCache) {
188            folder = mFolderCache.get(name);
189            if (folder == null) {
190                folder = new ImapFolder(name);
191                mFolderCache.put(name, folder);
192            }
193        }
194        return folder;
195    }
196
197
198    @Override
199    public Folder[] getPersonalNamespaces() throws MessagingException {
200        ImapConnection connection = getConnection();
201        try {
202            ArrayList<Folder> folders = new ArrayList<Folder>();
203            List<ImapResponse> responses =
204                    connection.executeSimpleCommand(String.format("LIST \"\" \"%s*\"",
205                        mPathPrefix == null ? "" : mPathPrefix));
206            for (ImapResponse response : responses) {
207                if (response.get(0).equals("LIST")) {
208                    boolean includeFolder = true;
209                    String folder = decodeFolderName(response.getString(3));
210                    if (folder.equalsIgnoreCase("INBOX")) {
211                        continue;
212                    }
213                    ImapList attributes = response.getList(1);
214                    for (int i = 0, count = attributes.size(); i < count; i++) {
215                        String attribute = attributes.getString(i);
216                        if (attribute.equalsIgnoreCase("\\NoSelect")) {
217                            includeFolder = false;
218                        }
219                    }
220                    if (includeFolder) {
221                        folders.add(getFolder(folder));
222                    }
223                }
224            }
225            folders.add(getFolder("INBOX"));
226            return folders.toArray(new Folder[] {});
227        } catch (IOException ioe) {
228            connection.close();
229            throw new MessagingException("Unable to get folder list.", ioe);
230        } finally {
231            releaseConnection(connection);
232        }
233    }
234
235    @Override
236    public void checkSettings() throws MessagingException {
237        try {
238            ImapConnection connection = new ImapConnection();
239            connection.open();
240            connection.close();
241        }
242        catch (IOException ioe) {
243            throw new MessagingException(MessagingException.IOERROR, ioe.toString());
244        }
245    }
246
247    /**
248     * Gets a connection if one is available for reuse, or creates a new one if not.
249     * @return
250     */
251    private ImapConnection getConnection() throws MessagingException {
252        synchronized (mConnections) {
253            ImapConnection connection = null;
254            while ((connection = mConnections.poll()) != null) {
255                try {
256                    connection.executeSimpleCommand("NOOP");
257                    break;
258                }
259                catch (IOException ioe) {
260                    connection.close();
261                }
262            }
263            if (connection == null) {
264                connection = new ImapConnection();
265            }
266            return connection;
267        }
268    }
269
270    private void releaseConnection(ImapConnection connection) {
271        mConnections.offer(connection);
272    }
273
274    private String encodeFolderName(String name) {
275        try {
276            ByteBuffer bb = mModifiedUtf7Charset.encode(name);
277            byte[] b = new byte[bb.limit()];
278            bb.get(b);
279            return new String(b, "US-ASCII");
280        }
281        catch (UnsupportedEncodingException uee) {
282            /*
283             * The only thing that can throw this is getBytes("US-ASCII") and if US-ASCII doesn't
284             * exist we're totally screwed.
285             */
286            throw new RuntimeException("Unabel to encode folder name: " + name, uee);
287        }
288    }
289
290    private String decodeFolderName(String name) {
291        /*
292         * Convert the encoded name to US-ASCII, then pass it through the modified UTF-7
293         * decoder and return the Unicode String.
294         */
295        try {
296            byte[] encoded = name.getBytes("US-ASCII");
297            CharBuffer cb = mModifiedUtf7Charset.decode(ByteBuffer.wrap(encoded));
298            return cb.toString();
299        }
300        catch (UnsupportedEncodingException uee) {
301            /*
302             * The only thing that can throw this is getBytes("US-ASCII") and if US-ASCII doesn't
303             * exist we're totally screwed.
304             */
305            throw new RuntimeException("Unable to decode folder name: " + name, uee);
306        }
307    }
308
309    class ImapFolder extends Folder {
310        private String mName;
311        private int mMessageCount = -1;
312        private ImapConnection mConnection;
313        private OpenMode mMode;
314        private boolean mExists;
315
316        public ImapFolder(String name) {
317            this.mName = name;
318        }
319
320        public void open(OpenMode mode) throws MessagingException {
321            if (isOpen() && mMode == mode) {
322                // Make sure the connection is valid. If it's not we'll close it down and continue
323                // on to get a new one.
324                try {
325                    mConnection.executeSimpleCommand("NOOP");
326                    return;
327                }
328                catch (IOException ioe) {
329                    ioExceptionHandler(mConnection, ioe);
330                }
331            }
332            synchronized (this) {
333                mConnection = getConnection();
334            }
335            // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft NonJunk
336            // $MDNSent)
337            // * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft
338            // NonJunk $MDNSent \*)] Flags permitted.
339            // * 23 EXISTS
340            // * 0 RECENT
341            // * OK [UIDVALIDITY 1125022061] UIDs valid
342            // * OK [UIDNEXT 57576] Predicted next UID
343            // 2 OK [READ-WRITE] Select completed.
344            try {
345                List<ImapResponse> responses = mConnection.executeSimpleCommand(
346                        String.format("SELECT \"%s\"",
347                                encodeFolderName(mName)));
348                /*
349                 * If the command succeeds we expect the folder has been opened read-write
350                 * unless we are notified otherwise in the responses.
351                 */
352                mMode = OpenMode.READ_WRITE;
353
354                for (ImapResponse response : responses) {
355                    if (response.mTag == null && response.get(1).equals("EXISTS")) {
356                        mMessageCount = response.getNumber(0);
357                    }
358                    else if (response.mTag != null && response.size() >= 2) {
359                        if ("[READ-ONLY]".equalsIgnoreCase(response.getString(1))) {
360                            mMode = OpenMode.READ_ONLY;
361                        }
362                        else if ("[READ-WRITE]".equalsIgnoreCase(response.getString(1))) {
363                            mMode = OpenMode.READ_WRITE;
364                        }
365                    }
366                }
367
368                if (mMessageCount == -1) {
369                    throw new MessagingException(
370                            "Did not find message count during select");
371                }
372                mExists = true;
373
374            } catch (IOException ioe) {
375                throw ioExceptionHandler(mConnection, ioe);
376            }
377        }
378
379        public boolean isOpen() {
380            return mConnection != null;
381        }
382
383        @Override
384        public OpenMode getMode() throws MessagingException {
385            return mMode;
386        }
387
388        public void close(boolean expunge) {
389            if (!isOpen()) {
390                return;
391            }
392            // TODO implement expunge
393            mMessageCount = -1;
394            synchronized (this) {
395                releaseConnection(mConnection);
396                mConnection = null;
397            }
398        }
399
400        public String getName() {
401            return mName;
402        }
403
404        public boolean exists() throws MessagingException {
405            if (mExists) {
406                return true;
407            }
408            /*
409             * This method needs to operate in the unselected mode as well as the selected mode
410             * so we must get the connection ourselves if it's not there. We are specifically
411             * not calling checkOpen() since we don't care if the folder is open.
412             */
413            ImapConnection connection = null;
414            synchronized(this) {
415                if (mConnection == null) {
416                    connection = getConnection();
417                }
418                else {
419                    connection = mConnection;
420                }
421            }
422            try {
423                connection.executeSimpleCommand(String.format("STATUS \"%s\" (UIDVALIDITY)",
424                        encodeFolderName(mName)));
425                mExists = true;
426                return true;
427            }
428            catch (MessagingException me) {
429                return false;
430            }
431            catch (IOException ioe) {
432                throw ioExceptionHandler(connection, ioe);
433            }
434            finally {
435                if (mConnection == null) {
436                    releaseConnection(connection);
437                }
438            }
439        }
440
441        public boolean create(FolderType type) throws MessagingException {
442            /*
443             * This method needs to operate in the unselected mode as well as the selected mode
444             * so we must get the connection ourselves if it's not there. We are specifically
445             * not calling checkOpen() since we don't care if the folder is open.
446             */
447            ImapConnection connection = null;
448            synchronized(this) {
449                if (mConnection == null) {
450                    connection = getConnection();
451                }
452                else {
453                    connection = mConnection;
454                }
455            }
456            try {
457                connection.executeSimpleCommand(String.format("CREATE \"%s\"",
458                        encodeFolderName(mName)));
459                return true;
460            }
461            catch (MessagingException me) {
462                return false;
463            }
464            catch (IOException ioe) {
465                throw ioExceptionHandler(mConnection, ioe);
466            }
467            finally {
468                if (mConnection == null) {
469                    releaseConnection(connection);
470                }
471            }
472        }
473
474        @Override
475        public void copyMessages(Message[] messages, Folder folder) throws MessagingException {
476            checkOpen();
477            String[] uids = new String[messages.length];
478            for (int i = 0, count = messages.length; i < count; i++) {
479                uids[i] = messages[i].getUid();
480            }
481            try {
482                mConnection.executeSimpleCommand(String.format("UID COPY %s \"%s\"",
483                        Utility.combine(uids, ','),
484                        encodeFolderName(folder.getName())));
485            }
486            catch (IOException ioe) {
487                throw ioExceptionHandler(mConnection, ioe);
488            }
489        }
490
491        @Override
492        public int getMessageCount() {
493            return mMessageCount;
494        }
495
496        @Override
497        public int getUnreadMessageCount() throws MessagingException {
498            checkOpen();
499            try {
500                int unreadMessageCount = 0;
501                List<ImapResponse> responses = mConnection.executeSimpleCommand(
502                        String.format("STATUS \"%s\" (UNSEEN)",
503                                encodeFolderName(mName)));
504                for (ImapResponse response : responses) {
505                    if (response.mTag == null && response.get(0).equals("STATUS")) {
506                        ImapList status = response.getList(2);
507                        unreadMessageCount = status.getKeyedNumber("UNSEEN");
508                    }
509                }
510                return unreadMessageCount;
511            }
512            catch (IOException ioe) {
513                throw ioExceptionHandler(mConnection, ioe);
514            }
515        }
516
517        @Override
518        public void delete(boolean recurse) throws MessagingException {
519            throw new Error("ImapStore.delete() not yet implemented");
520        }
521
522        @Override
523        public Message getMessage(String uid) throws MessagingException {
524            checkOpen();
525
526            try {
527                try {
528                    List<ImapResponse> responses =
529                            mConnection.executeSimpleCommand(String.format("UID SEARCH UID %S", uid));
530                    for (ImapResponse response : responses) {
531                        if (response.mTag == null && response.get(0).equals("SEARCH")) {
532                            for (int i = 1, count = response.size(); i < count; i++) {
533                                if (uid.equals(response.get(i))) {
534                                    return new ImapMessage(uid, this);
535                                }
536                            }
537                        }
538                    }
539                }
540                catch (MessagingException me) {
541                    return null;
542                }
543            }
544            catch (IOException ioe) {
545                throw ioExceptionHandler(mConnection, ioe);
546            }
547            return null;
548        }
549
550        @Override
551        public Message[] getMessages(int start, int end, MessageRetrievalListener listener)
552                throws MessagingException {
553            if (start < 1 || end < 1 || end < start) {
554                throw new MessagingException(
555                        String.format("Invalid message set %d %d",
556                                start, end));
557            }
558            checkOpen();
559            ArrayList<Message> messages = new ArrayList<Message>();
560            try {
561                ArrayList<String> uids = new ArrayList<String>();
562                List<ImapResponse> responses = mConnection
563                        .executeSimpleCommand(String.format("UID SEARCH %d:%d NOT DELETED", start, end));
564                for (ImapResponse response : responses) {
565                    if (response.get(0).equals("SEARCH")) {
566                        for (int i = 1, count = response.size(); i < count; i++) {
567                            uids.add(response.getString(i));
568                        }
569                    }
570                }
571                for (int i = 0, count = uids.size(); i < count; i++) {
572                    if (listener != null) {
573                        listener.messageStarted(uids.get(i), i, count);
574                    }
575                    ImapMessage message = new ImapMessage(uids.get(i), this);
576                    messages.add(message);
577                    if (listener != null) {
578                        listener.messageFinished(message, i, count);
579                    }
580                }
581            } catch (IOException ioe) {
582                throw ioExceptionHandler(mConnection, ioe);
583            }
584            return messages.toArray(new Message[] {});
585        }
586
587        public Message[] getMessages(MessageRetrievalListener listener) throws MessagingException {
588            return getMessages(null, listener);
589        }
590
591        public Message[] getMessages(String[] uids, MessageRetrievalListener listener)
592                throws MessagingException {
593            checkOpen();
594            ArrayList<Message> messages = new ArrayList<Message>();
595            try {
596                if (uids == null) {
597                    List<ImapResponse> responses = mConnection
598                            .executeSimpleCommand("UID SEARCH 1:* NOT DELETED");
599                    ArrayList<String> tempUids = new ArrayList<String>();
600                    for (ImapResponse response : responses) {
601                        if (response.get(0).equals("SEARCH")) {
602                            for (int i = 1, count = response.size(); i < count; i++) {
603                                tempUids.add(response.getString(i));
604                            }
605                        }
606                    }
607                    uids = tempUids.toArray(new String[] {});
608                }
609                for (int i = 0, count = uids.length; i < count; i++) {
610                    if (listener != null) {
611                        listener.messageStarted(uids[i], i, count);
612                    }
613                    ImapMessage message = new ImapMessage(uids[i], this);
614                    messages.add(message);
615                    if (listener != null) {
616                        listener.messageFinished(message, i, count);
617                    }
618                }
619            } catch (IOException ioe) {
620                throw ioExceptionHandler(mConnection, ioe);
621            }
622            return messages.toArray(new Message[] {});
623        }
624
625        public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)
626                throws MessagingException {
627            if (messages == null || messages.length == 0) {
628                return;
629            }
630            checkOpen();
631            String[] uids = new String[messages.length];
632            HashMap<String, Message> messageMap = new HashMap<String, Message>();
633            for (int i = 0, count = messages.length; i < count; i++) {
634                uids[i] = messages[i].getUid();
635                messageMap.put(uids[i], messages[i]);
636            }
637
638            /*
639             * Figure out what command we are going to run:
640             * Flags - UID FETCH (FLAGS)
641             * Envelope - UID FETCH ([FLAGS] INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[HEADER.FIELDS (date subject from content-type to cc)])
642             *
643             */
644            LinkedHashSet<String> fetchFields = new LinkedHashSet<String>();
645            fetchFields.add("UID");
646            if (fp.contains(FetchProfile.Item.FLAGS)) {
647                fetchFields.add("FLAGS");
648            }
649            if (fp.contains(FetchProfile.Item.ENVELOPE)) {
650                fetchFields.add("INTERNALDATE");
651                fetchFields.add("RFC822.SIZE");
652                fetchFields.add("BODY.PEEK[HEADER.FIELDS (date subject from content-type to cc)]");
653            }
654            if (fp.contains(FetchProfile.Item.STRUCTURE)) {
655                fetchFields.add("BODYSTRUCTURE");
656            }
657            if (fp.contains(FetchProfile.Item.BODY_SANE)) {
658                fetchFields.add(String.format("BODY.PEEK[]<0.%d>", FETCH_BODY_SANE_SUGGESTED_SIZE));
659            }
660            if (fp.contains(FetchProfile.Item.BODY)) {
661                fetchFields.add("BODY.PEEK[]");
662            }
663            for (Object o : fp) {
664                if (o instanceof Part) {
665                    Part part = (Part) o;
666                    String partId = part.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA)[0];
667                    fetchFields.add("BODY.PEEK[" + partId + "]");
668                }
669            }
670
671            try {
672                String tag = mConnection.sendCommand(String.format("UID FETCH %s (%s)",
673                        Utility.combine(uids, ','),
674                        Utility.combine(fetchFields.toArray(new String[fetchFields.size()]), ' ')
675                        ), false);
676                ImapResponse response;
677                int messageNumber = 0;
678                do {
679                    response = mConnection.readResponse();
680
681                    if (response.mTag == null && response.get(1).equals("FETCH")) {
682                        ImapList fetchList = (ImapList)response.getKeyedValue("FETCH");
683                        String uid = fetchList.getKeyedString("UID");
684
685                        Message message = messageMap.get(uid);
686
687                        if (listener != null) {
688                            listener.messageStarted(uid, messageNumber++, messageMap.size());
689                        }
690
691                        if (fp.contains(FetchProfile.Item.FLAGS)) {
692                            ImapList flags = fetchList.getKeyedList("FLAGS");
693                            ImapMessage imapMessage = (ImapMessage) message;
694                            if (flags != null) {
695                                for (int i = 0, count = flags.size(); i < count; i++) {
696                                    String flag = flags.getString(i);
697                                    if (flag.equals("\\Deleted")) {
698                                        imapMessage.setFlagInternal(Flag.DELETED, true);
699                                    }
700                                    else if (flag.equals("\\Answered")) {
701                                        imapMessage.setFlagInternal(Flag.ANSWERED, true);
702                                    }
703                                    else if (flag.equals("\\Seen")) {
704                                        imapMessage.setFlagInternal(Flag.SEEN, true);
705                                    }
706                                    else if (flag.equals("\\Flagged")) {
707                                        imapMessage.setFlagInternal(Flag.FLAGGED, true);
708                                    }
709                                }
710                            }
711                        }
712                        if (fp.contains(FetchProfile.Item.ENVELOPE)) {
713                            Date internalDate = fetchList.getKeyedDate("INTERNALDATE");
714                            int size = fetchList.getKeyedNumber("RFC822.SIZE");
715                            InputStream headerStream = fetchList.getLiteral(fetchList.size() - 1);
716
717                            ImapMessage imapMessage = (ImapMessage) message;
718
719                            message.setInternalDate(internalDate);
720                            imapMessage.setSize(size);
721                            imapMessage.parse(headerStream);
722                        }
723                        if (fp.contains(FetchProfile.Item.STRUCTURE)) {
724                            ImapList bs = fetchList.getKeyedList("BODYSTRUCTURE");
725                            if (bs != null) {
726                                try {
727                                    parseBodyStructure(bs, message, "TEXT");
728                                }
729                                catch (MessagingException e) {
730                                    if (Config.LOGV) {
731                                        Log.v(Email.LOG_TAG, "Error handling message", e);
732                                    }
733                                    message.setBody(null);
734                                }
735                            }
736                        }
737                        if (fp.contains(FetchProfile.Item.BODY)) {
738                            InputStream bodyStream = fetchList.getLiteral(fetchList.size() - 1);
739                            ImapMessage imapMessage = (ImapMessage) message;
740                            imapMessage.parse(bodyStream);
741                        }
742                        if (fp.contains(FetchProfile.Item.BODY_SANE)) {
743                            InputStream bodyStream = fetchList.getLiteral(fetchList.size() - 1);
744                            ImapMessage imapMessage = (ImapMessage) message;
745                            imapMessage.parse(bodyStream);
746                        }
747                        for (Object o : fp) {
748                            if (o instanceof Part) {
749                                Part part = (Part) o;
750                                InputStream bodyStream = fetchList.getLiteral(fetchList.size() - 1);
751                                String contentType = part.getContentType();
752                                String contentTransferEncoding = part.getHeader(
753                                        MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING)[0];
754                                part.setBody(MimeUtility.decodeBody(
755                                        bodyStream,
756                                        contentTransferEncoding));
757                            }
758                        }
759
760                        if (listener != null) {
761                            listener.messageFinished(message, messageNumber, messageMap.size());
762                        }
763                    }
764
765                    while (response.more());
766
767                } while (response.mTag == null);
768            }
769            catch (IOException ioe) {
770                throw ioExceptionHandler(mConnection, ioe);
771            }
772        }
773
774        @Override
775        public Flag[] getPermanentFlags() throws MessagingException {
776            return PERMANENT_FLAGS;
777        }
778
779        /**
780         * Handle any untagged responses that the caller doesn't care to handle themselves.
781         * @param responses
782         */
783        private void handleUntaggedResponses(List<ImapResponse> responses) {
784            for (ImapResponse response : responses) {
785                handleUntaggedResponse(response);
786            }
787        }
788
789        /**
790         * Handle an untagged response that the caller doesn't care to handle themselves.
791         * @param response
792         */
793        private void handleUntaggedResponse(ImapResponse response) {
794            if (response.mTag == null && response.get(1).equals("EXISTS")) {
795                mMessageCount = response.getNumber(0);
796            }
797        }
798
799        private void parseBodyStructure(ImapList bs, Part part, String id)
800                throws MessagingException {
801            if (bs.get(0) instanceof ImapList) {
802                /*
803                 * This is a multipart/*
804                 */
805                MimeMultipart mp = new MimeMultipart();
806                for (int i = 0, count = bs.size(); i < count; i++) {
807                    if (bs.get(i) instanceof ImapList) {
808                        /*
809                         * For each part in the message we're going to add a new BodyPart and parse
810                         * into it.
811                         */
812                        ImapBodyPart bp = new ImapBodyPart();
813                        if (id.equals("TEXT")) {
814                            parseBodyStructure(bs.getList(i), bp, Integer.toString(i + 1));
815                        }
816                        else {
817                            parseBodyStructure(bs.getList(i), bp, id + "." + (i + 1));
818                        }
819                        mp.addBodyPart(bp);
820                    }
821                    else {
822                        /*
823                         * We've got to the end of the children of the part, so now we can find out
824                         * what type it is and bail out.
825                         */
826                        String subType = bs.getString(i);
827                        mp.setSubType(subType.toLowerCase());
828                        break;
829                    }
830                }
831                part.setBody(mp);
832            }
833            else{
834                /*
835                 * This is a body. We need to add as much information as we can find out about
836                 * it to the Part.
837                 */
838
839                /*
840                 body type
841                 body subtype
842                 body parameter parenthesized list
843                 body id
844                 body description
845                 body encoding
846                 body size
847                 */
848
849
850                String type = bs.getString(0);
851                String subType = bs.getString(1);
852                String mimeType = (type + "/" + subType).toLowerCase();
853
854                ImapList bodyParams = null;
855                if (bs.get(2) instanceof ImapList) {
856                    bodyParams = bs.getList(2);
857                }
858                String encoding = bs.getString(5);
859                int size = bs.getNumber(6);
860
861                if (MimeUtility.mimeTypeMatches(mimeType, "message/rfc822")) {
862//                  A body type of type MESSAGE and subtype RFC822
863//                  contains, immediately after the basic fields, the
864//                  envelope structure, body structure, and size in
865//                  text lines of the encapsulated message.
866//                    [MESSAGE, RFC822, [NAME, Fwd: [#HTR-517941]:  update plans at 1am Friday - Memory allocation - displayware.eml], NIL, NIL, 7BIT, 5974, NIL, [INLINE, [FILENAME*0, Fwd: [#HTR-517941]:  update plans at 1am Friday - Memory all, FILENAME*1, ocation - displayware.eml]], NIL]
867                    /*
868                     * This will be caught by fetch and handled appropriately.
869                     */
870                    throw new MessagingException("BODYSTRUCTURE message/rfc822 not yet supported.");
871                }
872
873                /*
874                 * Set the content type with as much information as we know right now.
875                 */
876                String contentType = String.format("%s", mimeType);
877
878                if (bodyParams != null) {
879                    /*
880                     * If there are body params we might be able to get some more information out
881                     * of them.
882                     */
883                    for (int i = 0, count = bodyParams.size(); i < count; i += 2) {
884                        contentType += String.format(";\n %s=\"%s\"",
885                                bodyParams.getString(i),
886                                bodyParams.getString(i + 1));
887                    }
888                }
889
890                part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType);
891
892                // Extension items
893                ImapList bodyDisposition = null;
894                if (("text".equalsIgnoreCase(type))
895                        && (bs.size() > 8)
896                        && (bs.get(9) instanceof ImapList)) {
897                    bodyDisposition = bs.getList(9);
898                }
899                else if (!("text".equalsIgnoreCase(type))
900                        && (bs.size() > 7)
901                        && (bs.get(8) instanceof ImapList)) {
902                    bodyDisposition = bs.getList(8);
903                }
904
905                String contentDisposition = "";
906
907                if (bodyDisposition != null && bodyDisposition.size() > 0) {
908                    if (!"NIL".equalsIgnoreCase(bodyDisposition.getString(0))) {
909                        contentDisposition = bodyDisposition.getString(0).toLowerCase();
910                    }
911
912                    if ((bodyDisposition.size() > 1)
913                            && (bodyDisposition.get(1) instanceof ImapList)) {
914                        ImapList bodyDispositionParams = bodyDisposition.getList(1);
915                        /*
916                         * If there is body disposition information we can pull some more information
917                         * about the attachment out.
918                         */
919                        for (int i = 0, count = bodyDispositionParams.size(); i < count; i += 2) {
920                            contentDisposition += String.format(";\n %s=\"%s\"",
921                                    bodyDispositionParams.getString(i).toLowerCase(),
922                                    bodyDispositionParams.getString(i + 1));
923                        }
924                    }
925                }
926
927                if (MimeUtility.getHeaderParameter(contentDisposition, "size") == null) {
928                    contentDisposition += String.format(";\n size=%d", size);
929                }
930
931                /*
932                 * Set the content disposition containing at least the size. Attachment
933                 * handling code will use this down the road.
934                 */
935                part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, contentDisposition);
936
937
938                /*
939                 * Set the Content-Transfer-Encoding header. Attachment code will use this
940                 * to parse the body.
941                 */
942                part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, encoding);
943
944                if (part instanceof ImapMessage) {
945                    ((ImapMessage) part).setSize(size);
946                }
947                else if (part instanceof ImapBodyPart) {
948                    ((ImapBodyPart) part).setSize(size);
949                }
950                else {
951                    throw new MessagingException("Unknown part type " + part.toString());
952                }
953                part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id);
954            }
955
956        }
957
958        /**
959         * Appends the given messages to the selected folder. This implementation also determines
960         * the new UID of the given message on the IMAP server and sets the Message's UID to the
961         * new server UID.
962         */
963        public void appendMessages(Message[] messages) throws MessagingException {
964            checkOpen();
965            try {
966                for (Message message : messages) {
967                    CountingOutputStream out = new CountingOutputStream();
968                    EOLConvertingOutputStream eolOut = new EOLConvertingOutputStream(out);
969                    message.writeTo(eolOut);
970                    eolOut.flush();
971                    mConnection.sendCommand(
972                            String.format("APPEND \"%s\" {%d}",
973                                    encodeFolderName(mName),
974                                    out.getCount()), false);
975                    ImapResponse response;
976                    do {
977                        response = mConnection.readResponse();
978                        if (response.mCommandContinuationRequested) {
979                            eolOut = new EOLConvertingOutputStream(mConnection.mTransport.getOutputStream());
980                            message.writeTo(eolOut);
981                            eolOut.write('\r');
982                            eolOut.write('\n');
983                            eolOut.flush();
984                        }
985                        else if (response.mTag == null) {
986                            handleUntaggedResponse(response);
987                        }
988                        while (response.more());
989                    } while(response.mTag == null);
990
991                    /*
992                     * Try to find the UID of the message we just appended using the
993                     * Message-ID header.
994                     */
995                    String[] messageIdHeader = message.getHeader("Message-ID");
996                    if (messageIdHeader == null || messageIdHeader.length == 0) {
997                        continue;
998                    }
999                    String messageId = messageIdHeader[0];
1000                    List<ImapResponse> responses =
1001                        mConnection.executeSimpleCommand(
1002                                String.format("UID SEARCH (HEADER MESSAGE-ID %s)", messageId));
1003                    for (ImapResponse response1 : responses) {
1004                        if (response1.mTag == null && response1.get(0).equals("SEARCH")
1005                                && response1.size() > 1) {
1006                            message.setUid(response1.getString(1));
1007                        }
1008                    }
1009
1010                }
1011            }
1012            catch (IOException ioe) {
1013                throw ioExceptionHandler(mConnection, ioe);
1014            }
1015        }
1016
1017        public Message[] expunge() throws MessagingException {
1018            checkOpen();
1019            try {
1020                handleUntaggedResponses(mConnection.executeSimpleCommand("EXPUNGE"));
1021            } catch (IOException ioe) {
1022                throw ioExceptionHandler(mConnection, ioe);
1023            }
1024            return null;
1025        }
1026
1027        public void setFlags(Message[] messages, Flag[] flags, boolean value)
1028                throws MessagingException {
1029            checkOpen();
1030            String[] uids = new String[messages.length];
1031            for (int i = 0, count = messages.length; i < count; i++) {
1032                uids[i] = messages[i].getUid();
1033            }
1034            ArrayList<String> flagNames = new ArrayList<String>();
1035            for (int i = 0, count = flags.length; i < count; i++) {
1036                Flag flag = flags[i];
1037                if (flag == Flag.SEEN) {
1038                    flagNames.add("\\Seen");
1039                }
1040                else if (flag == Flag.DELETED) {
1041                    flagNames.add("\\Deleted");
1042                }
1043            }
1044            try {
1045                mConnection.executeSimpleCommand(String.format("UID STORE %s %sFLAGS.SILENT (%s)",
1046                        Utility.combine(uids, ','),
1047                        value ? "+" : "-",
1048                        Utility.combine(flagNames.toArray(new String[flagNames.size()]), ' ')));
1049            }
1050            catch (IOException ioe) {
1051                throw ioExceptionHandler(mConnection, ioe);
1052            }
1053        }
1054
1055        private void checkOpen() throws MessagingException {
1056            if (!isOpen()) {
1057                throw new MessagingException("Folder " + mName + " is not open.");
1058            }
1059        }
1060
1061        private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe)
1062                throws MessagingException {
1063            connection.close();
1064            close(false);
1065            return new MessagingException("IO Error", ioe);
1066        }
1067
1068        @Override
1069        public boolean equals(Object o) {
1070            if (o instanceof ImapFolder) {
1071                return ((ImapFolder)o).mName.equals(mName);
1072            }
1073            return super.equals(o);
1074        }
1075    }
1076
1077    /**
1078     * A cacheable class that stores the details for a single IMAP connection.
1079     */
1080    class ImapConnection {
1081        private Transport mTransport;
1082        private ImapResponseParser mParser;
1083        private int mNextCommandTag;
1084
1085        public void open() throws IOException, MessagingException {
1086            PeekableInputStream mIn;
1087
1088            if (mTransport != null && mTransport.isOpen()) {
1089                return;
1090            }
1091
1092            mNextCommandTag = 1;
1093
1094            try {
1095                // copy configuration into a clean transport, if necessary
1096                if (mTransport == null) {
1097                    mTransport = mRootTransport.newInstanceWithConfiguration();
1098                }
1099
1100                mTransport.open();
1101                mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT);
1102
1103                mIn = new PeekableInputStream(mTransport.getInputStream());
1104                mParser = new ImapResponseParser(mIn);
1105
1106                // BANNER
1107                mParser.readResponse();
1108
1109                if (mTransport.canTryTlsSecurity()) {
1110                    // CAPABILITY
1111                    List<ImapResponse> responses = executeSimpleCommand("CAPABILITY");
1112                    if (responses.size() != 2) {
1113                        throw new MessagingException("Invalid CAPABILITY response received");
1114                    }
1115                    if (responses.get(0).contains("STARTTLS")) {
1116                        // STARTTLS
1117                        executeSimpleCommand("STARTTLS");
1118
1119                        mTransport.reopenTls();
1120                        mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT);
1121                        mIn = new PeekableInputStream(mTransport.getInputStream());
1122                        mParser = new ImapResponseParser(mIn);
1123                    } else if (mTransport.getSecurity() ==
1124                            Transport.CONNECTION_SECURITY_TLS_REQUIRED) {
1125                        if (Config.LOGD && Email.DEBUG) {
1126                            Log.d(Email.LOG_TAG, "TLS not supported but required");
1127                        }
1128                        throw new MessagingException(MessagingException.TLS_REQUIRED);
1129                    }
1130                }
1131
1132                try {
1133                    // TODO eventually we need to add additional authentication
1134                    // options such as SASL
1135                    executeSimpleCommand(mLoginPhrase, true);
1136                } catch (ImapException ie) {
1137                    if (Config.LOGD && Email.DEBUG) {
1138                        Log.d(Email.LOG_TAG, ie.toString());
1139                    }
1140                    throw new AuthenticationFailedException(ie.getAlertText(), ie);
1141
1142                } catch (MessagingException me) {
1143                    throw new AuthenticationFailedException(null, me);
1144                }
1145            } catch (SSLException e) {
1146                if (Config.LOGD && Email.DEBUG) {
1147                    Log.d(Email.LOG_TAG, e.toString());
1148                }
1149                throw new CertificateValidationException(e.getMessage(), e);
1150            } catch (IOException ioe) {
1151                // NOTE:  Unlike similar code in POP3, I'm going to rethrow as-is.  There is a lot
1152                // of other code here that catches IOException and I don't want to break it.
1153                // This catch is only here to enhance logging of connection-time issues.
1154                if (Config.LOGD && Email.DEBUG) {
1155                    Log.d(Email.LOG_TAG, ioe.toString());
1156                }
1157                throw ioe;
1158            }
1159        }
1160
1161        public void close() {
1162//            if (isOpen()) {
1163//                try {
1164//                    executeSimpleCommand("LOGOUT");
1165//                } catch (Exception e) {
1166//
1167//                }
1168//            }
1169            if (mTransport != null) {
1170                mTransport.close();
1171            }
1172        }
1173
1174        public ImapResponse readResponse() throws IOException, MessagingException {
1175            return mParser.readResponse();
1176        }
1177
1178        /**
1179         * Send a single command to the server.  The command will be preceded by an IMAP command
1180         * tag and followed by \r\n (caller need not supply them).
1181         *
1182         * @param command The command to send to the server
1183         * @param sensitive If true, the command will not be logged
1184         * @return Returns the command tag that was sent
1185         */
1186        public String sendCommand(String command, boolean sensitive)
1187            throws MessagingException, IOException {
1188            open();
1189            String tag = Integer.toString(mNextCommandTag++);
1190            String commandToSend = tag + " " + command;
1191            mTransport.writeLine(commandToSend, sensitive ? "[IMAP command redacted]" : null);
1192            return tag;
1193        }
1194
1195        public List<ImapResponse> executeSimpleCommand(String command) throws IOException,
1196                ImapException, MessagingException {
1197            return executeSimpleCommand(command, false);
1198        }
1199
1200        public List<ImapResponse> executeSimpleCommand(String command, boolean sensitive)
1201                throws IOException, ImapException, MessagingException {
1202            String tag = sendCommand(command, sensitive);
1203            ArrayList<ImapResponse> responses = new ArrayList<ImapResponse>();
1204            ImapResponse response;
1205            do {
1206                response = mParser.readResponse();
1207                responses.add(response);
1208            } while (response.mTag == null);
1209            if (response.size() < 1 || !response.get(0).equals("OK")) {
1210                throw new ImapException(response.toString(), response.getAlertText());
1211            }
1212            return responses;
1213        }
1214    }
1215
1216    class ImapMessage extends MimeMessage {
1217        ImapMessage(String uid, Folder folder) throws MessagingException {
1218            this.mUid = uid;
1219            this.mFolder = folder;
1220        }
1221
1222        public void setSize(int size) {
1223            this.mSize = size;
1224        }
1225
1226        public void parse(InputStream in) throws IOException, MessagingException {
1227            super.parse(in);
1228        }
1229
1230        public void setFlagInternal(Flag flag, boolean set) throws MessagingException {
1231            super.setFlag(flag, set);
1232        }
1233
1234        @Override
1235        public void setFlag(Flag flag, boolean set) throws MessagingException {
1236            super.setFlag(flag, set);
1237            mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set);
1238        }
1239    }
1240
1241    class ImapBodyPart extends MimeBodyPart {
1242        public ImapBodyPart() throws MessagingException {
1243            super();
1244        }
1245
1246        public void setSize(int size) {
1247            this.mSize = size;
1248        }
1249    }
1250
1251    class ImapException extends MessagingException {
1252        String mAlertText;
1253
1254        public ImapException(String message, String alertText, Throwable throwable) {
1255            super(message, throwable);
1256            this.mAlertText = alertText;
1257        }
1258
1259        public ImapException(String message, String alertText) {
1260            super(message);
1261            this.mAlertText = alertText;
1262        }
1263
1264        public String getAlertText() {
1265            return mAlertText;
1266        }
1267
1268        public void setAlertText(String alertText) {
1269            mAlertText = alertText;
1270        }
1271    }
1272}
1273