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