• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.email.mail.store;
18 
19 import android.text.TextUtils;
20 
21 import com.android.email.mail.store.ImapStore.ImapException;
22 import com.android.email.mail.store.imap.ImapConstants;
23 import com.android.email.mail.store.imap.ImapList;
24 import com.android.email.mail.store.imap.ImapResponse;
25 import com.android.email.mail.store.imap.ImapResponseParser;
26 import com.android.email.mail.store.imap.ImapUtility;
27 import com.android.email.mail.transport.DiscourseLogger;
28 import com.android.email.mail.transport.MailTransport;
29 import com.android.email2.ui.MailActivityEmail;
30 import com.android.emailcommon.Logging;
31 import com.android.emailcommon.mail.AuthenticationFailedException;
32 import com.android.emailcommon.mail.CertificateValidationException;
33 import com.android.emailcommon.mail.MessagingException;
34 import com.android.mail.utils.LogUtils;
35 
36 import java.io.IOException;
37 import java.util.ArrayList;
38 import java.util.Collections;
39 import java.util.List;
40 import java.util.concurrent.atomic.AtomicInteger;
41 
42 import javax.net.ssl.SSLException;
43 
44 /**
45  * A cacheable class that stores the details for a single IMAP connection.
46  */
47 class ImapConnection {
48     // Always check in FALSE
49     private static final boolean DEBUG_FORCE_SEND_ID = false;
50 
51     /** ID capability per RFC 2971*/
52     public static final int CAPABILITY_ID        = 1 << 0;
53     /** NAMESPACE capability per RFC 2342 */
54     public static final int CAPABILITY_NAMESPACE = 1 << 1;
55     /** STARTTLS capability per RFC 3501 */
56     public static final int CAPABILITY_STARTTLS  = 1 << 2;
57     /** UIDPLUS capability per RFC 4315 */
58     public static final int CAPABILITY_UIDPLUS   = 1 << 3;
59 
60     /** The capabilities supported; a set of CAPABILITY_* values. */
61     private int mCapabilities;
62     private static final String IMAP_REDACTED_LOG = "[IMAP command redacted]";
63     MailTransport mTransport;
64     private ImapResponseParser mParser;
65     private ImapStore mImapStore;
66     private String mUsername;
67     private String mLoginPhrase;
68     private String mIdPhrase = null;
69     /** # of command/response lines to log upon crash. */
70     private static final int DISCOURSE_LOGGER_SIZE = 64;
71     private final DiscourseLogger mDiscourse = new DiscourseLogger(DISCOURSE_LOGGER_SIZE);
72     /**
73      * Next tag to use.  All connections associated to the same ImapStore instance share the same
74      * counter to make tests simpler.
75      * (Some of the tests involve multiple connections but only have a single counter to track the
76      * tag.)
77      */
78     private final AtomicInteger mNextCommandTag = new AtomicInteger(0);
79 
80 
81     // Keep others from instantiating directly
ImapConnection(ImapStore store, String username, String password)82     ImapConnection(ImapStore store, String username, String password) {
83         setStore(store, username, password);
84     }
85 
setStore(ImapStore store, String username, String password)86     void setStore(ImapStore store, String username, String password) {
87         if (username != null && password != null) {
88             mUsername = username;
89 
90             // build the LOGIN string once (instead of over-and-over again.)
91             // apply the quoting here around the built-up password
92             mLoginPhrase = ImapConstants.LOGIN + " " + mUsername + " "
93                     + ImapUtility.imapQuoted(password);
94         }
95         mImapStore = store;
96     }
open()97     void open() throws IOException, MessagingException {
98         if (mTransport != null && mTransport.isOpen()) {
99             return;
100         }
101 
102         try {
103             // copy configuration into a clean transport, if necessary
104             if (mTransport == null) {
105                 mTransport = mImapStore.cloneTransport();
106             }
107 
108             mTransport.open();
109 
110             createParser();
111 
112             // BANNER
113             mParser.readResponse();
114 
115             // CAPABILITY
116             ImapResponse capabilities = queryCapabilities();
117 
118             boolean hasStartTlsCapability =
119                 capabilities.contains(ImapConstants.STARTTLS);
120 
121             // TLS
122             ImapResponse newCapabilities = doStartTls(hasStartTlsCapability);
123             if (newCapabilities != null) {
124                 capabilities = newCapabilities;
125             }
126 
127             // NOTE: An IMAP response MUST be processed before issuing any new IMAP
128             // requests. Subsequent requests may destroy previous response data. As
129             // such, we save away capability information here for future use.
130             setCapabilities(capabilities);
131             String capabilityString = capabilities.flatten();
132 
133             // ID
134             doSendId(isCapable(CAPABILITY_ID), capabilityString);
135 
136             // LOGIN
137             doLogin();
138 
139             // NAMESPACE (only valid in the Authenticated state)
140             doGetNamespace(isCapable(CAPABILITY_NAMESPACE));
141 
142             // Gets the path separator from the server
143             doGetPathSeparator();
144 
145             mImapStore.ensurePrefixIsValid();
146         } catch (SSLException e) {
147             if (MailActivityEmail.DEBUG) {
148                 LogUtils.d(Logging.LOG_TAG, e.toString());
149             }
150             throw new CertificateValidationException(e.getMessage(), e);
151         } catch (IOException ioe) {
152             // NOTE:  Unlike similar code in POP3, I'm going to rethrow as-is.  There is a lot
153             // of other code here that catches IOException and I don't want to break it.
154             // This catch is only here to enhance logging of connection-time issues.
155             if (MailActivityEmail.DEBUG) {
156                 LogUtils.d(Logging.LOG_TAG, ioe.toString());
157             }
158             throw ioe;
159         } finally {
160             destroyResponses();
161         }
162     }
163 
164     /**
165      * Closes the connection and releases all resources. This connection can not be used again
166      * until {@link #setStore(ImapStore, String, String)} is called.
167      */
close()168     void close() {
169         if (mTransport != null) {
170             mTransport.close();
171             mTransport = null;
172         }
173         destroyResponses();
174         mParser = null;
175         mImapStore = null;
176     }
177 
178     /**
179      * Returns whether or not the specified capability is supported by the server.
180      */
isCapable(int capability)181     private boolean isCapable(int capability) {
182         return (mCapabilities & capability) != 0;
183     }
184 
185     /**
186      * Sets the capability flags according to the response provided by the server.
187      * Note: We only set the capability flags that we are interested in. There are many IMAP
188      * capabilities that we do not track.
189      */
setCapabilities(ImapResponse capabilities)190     private void setCapabilities(ImapResponse capabilities) {
191         if (capabilities.contains(ImapConstants.ID)) {
192             mCapabilities |= CAPABILITY_ID;
193         }
194         if (capabilities.contains(ImapConstants.NAMESPACE)) {
195             mCapabilities |= CAPABILITY_NAMESPACE;
196         }
197         if (capabilities.contains(ImapConstants.UIDPLUS)) {
198             mCapabilities |= CAPABILITY_UIDPLUS;
199         }
200         if (capabilities.contains(ImapConstants.STARTTLS)) {
201             mCapabilities |= CAPABILITY_STARTTLS;
202         }
203     }
204 
205     /**
206      * Create an {@link ImapResponseParser} from {@code mTransport.getInputStream()} and
207      * set it to {@link #mParser}.
208      *
209      * If we already have an {@link ImapResponseParser}, we
210      * {@link #destroyResponses()} and throw it away.
211      */
createParser()212     private void createParser() {
213         destroyResponses();
214         mParser = new ImapResponseParser(mTransport.getInputStream(), mDiscourse);
215     }
216 
destroyResponses()217     void destroyResponses() {
218         if (mParser != null) {
219             mParser.destroyResponses();
220         }
221     }
222 
isTransportOpenForTest()223     boolean isTransportOpenForTest() {
224         return mTransport != null ? mTransport.isOpen() : false;
225     }
226 
readResponse()227     ImapResponse readResponse() throws IOException, MessagingException {
228         return mParser.readResponse();
229     }
230 
231     /**
232      * Send a single command to the server.  The command will be preceded by an IMAP command
233      * tag and followed by \r\n (caller need not supply them).
234      *
235      * @param command The command to send to the server
236      * @param sensitive If true, the command will not be logged
237      * @return Returns the command tag that was sent
238      */
sendCommand(String command, boolean sensitive)239     String sendCommand(String command, boolean sensitive)
240         throws MessagingException, IOException {
241         open();
242         String tag = Integer.toString(mNextCommandTag.incrementAndGet());
243         String commandToSend = tag + " " + command;
244         mTransport.writeLine(commandToSend, sensitive ? IMAP_REDACTED_LOG : null);
245         mDiscourse.addSentCommand(sensitive ? IMAP_REDACTED_LOG : commandToSend);
246         return tag;
247     }
248 
249 
250     /**
251      * Send a single, complex command to the server.  The command will be preceded by an IMAP
252      * command tag and followed by \r\n (caller need not supply them).  After each piece of the
253      * command, a response will be read which MUST be a continuation request.
254      *
255      * @param commands An array of Strings comprising the command to be sent to the server
256      * @return Returns the command tag that was sent
257      */
sendComplexCommand(List<String> commands, boolean sensitive)258     String sendComplexCommand(List<String> commands, boolean sensitive) throws MessagingException,
259             IOException {
260         open();
261         String tag = Integer.toString(mNextCommandTag.incrementAndGet());
262         int len = commands.size();
263         for (int i = 0; i < len; i++) {
264             String commandToSend = commands.get(i);
265             // The first part of the command gets the tag
266             if (i == 0) {
267                 commandToSend = tag + " " + commandToSend;
268             } else {
269                 // Otherwise, read the response from the previous part of the command
270                 ImapResponse response = readResponse();
271                 // If it isn't a continuation request, that's an error
272                 if (!response.isContinuationRequest()) {
273                     throw new MessagingException("Expected continuation request");
274                 }
275             }
276             // Send the command
277             mTransport.writeLine(commandToSend, null);
278             mDiscourse.addSentCommand(sensitive ? IMAP_REDACTED_LOG : commandToSend);
279         }
280         return tag;
281     }
282 
executeSimpleCommand(String command)283     List<ImapResponse> executeSimpleCommand(String command) throws IOException,
284             MessagingException {
285         return executeSimpleCommand(command, false);
286     }
287 
288     /**
289      * Read and return all of the responses from the most recent command sent to the server
290      *
291      * @return a list of ImapResponses
292      * @throws IOException
293      * @throws MessagingException
294      */
getCommandResponses()295     List<ImapResponse> getCommandResponses() throws IOException, MessagingException {
296         ArrayList<ImapResponse> responses = new ArrayList<ImapResponse>();
297         ImapResponse response;
298         do {
299             response = mParser.readResponse();
300             responses.add(response);
301         } while (!response.isTagged());
302         if (!response.isOk()) {
303             final String toString = response.toString();
304             final String alert = response.getAlertTextOrEmpty().getString();
305             destroyResponses();
306             throw new ImapException(toString, alert);
307         }
308         return responses;
309     }
310 
311     /**
312      * Execute a simple command at the server, a simple command being one that is sent in a single
313      * line of text
314      *
315      * @param command the command to send to the server
316      * @param sensitive whether the command should be redacted in logs (used for login)
317      * @return a list of ImapResponses
318      * @throws IOException
319      * @throws MessagingException
320      */
executeSimpleCommand(String command, boolean sensitive)321      List<ImapResponse> executeSimpleCommand(String command, boolean sensitive)
322             throws IOException, MessagingException {
323         sendCommand(command, sensitive);
324         return getCommandResponses();
325     }
326 
327      /**
328       * Execute a complex command at the server, a complex command being one that must be sent in
329       * multiple lines due to the use of string literals
330       *
331       * @param commands a list of strings that comprise the command to be sent to the server
332       * @param sensitive whether the command should be redacted in logs (used for login)
333       * @return a list of ImapResponses
334       * @throws IOException
335       * @throws MessagingException
336       */
executeComplexCommand(List<String> commands, boolean sensitive)337       List<ImapResponse> executeComplexCommand(List<String> commands, boolean sensitive)
338             throws IOException, MessagingException {
339         sendComplexCommand(commands, sensitive);
340         return getCommandResponses();
341     }
342 
343     /**
344      * Query server for capabilities.
345      */
queryCapabilities()346     private ImapResponse queryCapabilities() throws IOException, MessagingException {
347         ImapResponse capabilityResponse = null;
348         for (ImapResponse r : executeSimpleCommand(ImapConstants.CAPABILITY)) {
349             if (r.is(0, ImapConstants.CAPABILITY)) {
350                 capabilityResponse = r;
351                 break;
352             }
353         }
354         if (capabilityResponse == null) {
355             throw new MessagingException("Invalid CAPABILITY response received");
356         }
357         return capabilityResponse;
358     }
359 
360     /**
361      * Sends client identification information to the IMAP server per RFC 2971. If
362      * the server does not support the ID command, this will perform no operation.
363      *
364      * Interoperability hack:  Never send ID to *.secureserver.net, which sends back a
365      * malformed response that our parser can't deal with.
366      */
doSendId(boolean hasIdCapability, String capabilities)367     private void doSendId(boolean hasIdCapability, String capabilities)
368             throws MessagingException {
369         if (!hasIdCapability) return;
370 
371         // Never send ID to *.secureserver.net
372         String host = mTransport.getHost();
373         if (host.toLowerCase().endsWith(".secureserver.net")) return;
374 
375         // Assign user-agent string (for RFC2971 ID command)
376         String mUserAgent =
377                 ImapStore.getImapId(mImapStore.getContext(), mUsername, host, capabilities);
378 
379         if (mUserAgent != null) {
380             mIdPhrase = ImapConstants.ID + " (" + mUserAgent + ")";
381         } else if (DEBUG_FORCE_SEND_ID) {
382             mIdPhrase = ImapConstants.ID + " " + ImapConstants.NIL;
383         }
384         // else: mIdPhrase = null, no ID will be emitted
385 
386         // Send user-agent in an RFC2971 ID command
387         if (mIdPhrase != null) {
388             try {
389                 executeSimpleCommand(mIdPhrase);
390             } catch (ImapException ie) {
391                 // Log for debugging, but this is not a fatal problem.
392                 if (MailActivityEmail.DEBUG) {
393                     LogUtils.d(Logging.LOG_TAG, ie.toString());
394                 }
395             } catch (IOException ioe) {
396                 // Special case to handle malformed OK responses and ignore them.
397                 // A true IOException will recur on the following login steps
398                 // This can go away after the parser is fixed - see bug 2138981
399             }
400         }
401     }
402 
403     /**
404      * Gets the user's Personal Namespace from the IMAP server per RFC 2342. If the user
405      * explicitly sets a namespace (using setup UI) or if the server does not support the
406      * namespace command, this will perform no operation.
407      */
doGetNamespace(boolean hasNamespaceCapability)408     private void doGetNamespace(boolean hasNamespaceCapability) throws MessagingException {
409         // user did not specify a hard-coded prefix; try to get it from the server
410         if (hasNamespaceCapability && !mImapStore.isUserPrefixSet()) {
411             List<ImapResponse> responseList = Collections.emptyList();
412 
413             try {
414                 responseList = executeSimpleCommand(ImapConstants.NAMESPACE);
415             } catch (ImapException ie) {
416                 // Log for debugging, but this is not a fatal problem.
417                 if (MailActivityEmail.DEBUG) {
418                     LogUtils.d(Logging.LOG_TAG, ie.toString());
419                 }
420             } catch (IOException ioe) {
421                 // Special case to handle malformed OK responses and ignore them.
422             }
423 
424             for (ImapResponse response: responseList) {
425                 if (response.isDataResponse(0, ImapConstants.NAMESPACE)) {
426                     ImapList namespaceList = response.getListOrEmpty(1);
427                     ImapList namespace = namespaceList.getListOrEmpty(0);
428                     String namespaceString = namespace.getStringOrEmpty(0).getString();
429                     if (!TextUtils.isEmpty(namespaceString)) {
430                         mImapStore.setPathPrefix(ImapStore.decodeFolderName(namespaceString, null));
431                         mImapStore.setPathSeparator(namespace.getStringOrEmpty(1).getString());
432                     }
433                 }
434             }
435         }
436     }
437 
438     /**
439      * Logs into the IMAP server
440      */
doLogin()441     private void doLogin()
442             throws IOException, MessagingException, AuthenticationFailedException {
443         try {
444             // TODO eventually we need to add additional authentication
445             // options such as SASL
446             executeSimpleCommand(mLoginPhrase, true);
447         } catch (ImapException ie) {
448             if (MailActivityEmail.DEBUG) {
449                 LogUtils.d(Logging.LOG_TAG, ie.toString());
450             }
451             throw new AuthenticationFailedException(ie.getAlertText(), ie);
452 
453         } catch (MessagingException me) {
454             throw new AuthenticationFailedException(null, me);
455         }
456     }
457 
458     /**
459      * Gets the path separator per the LIST command in RFC 3501. If the path separator
460      * was obtained while obtaining the namespace or there is no prefix defined, this
461      * will perform no operation.
462      */
doGetPathSeparator()463     private void doGetPathSeparator() throws MessagingException {
464         // user did not specify a hard-coded prefix; try to get it from the server
465         if (mImapStore.isUserPrefixSet()) {
466             List<ImapResponse> responseList = Collections.emptyList();
467 
468             try {
469                 responseList = executeSimpleCommand(ImapConstants.LIST + " \"\" \"\"");
470             } catch (ImapException ie) {
471                 // Log for debugging, but this is not a fatal problem.
472                 if (MailActivityEmail.DEBUG) {
473                     LogUtils.d(Logging.LOG_TAG, ie.toString());
474                 }
475             } catch (IOException ioe) {
476                 // Special case to handle malformed OK responses and ignore them.
477             }
478 
479             for (ImapResponse response: responseList) {
480                 if (response.isDataResponse(0, ImapConstants.LIST)) {
481                     mImapStore.setPathSeparator(response.getStringOrEmpty(2).getString());
482                 }
483             }
484         }
485     }
486 
487     /**
488      * Starts a TLS session with the IMAP server per RFC 3501. If the user has not opted
489      * to use TLS or the server does not support the TLS capability, this will perform
490      * no operation.
491      */
doStartTls(boolean hasStartTlsCapability)492     private ImapResponse doStartTls(boolean hasStartTlsCapability)
493             throws IOException, MessagingException {
494         if (mTransport.canTryTlsSecurity()) {
495             if (hasStartTlsCapability) {
496                 // STARTTLS
497                 executeSimpleCommand(ImapConstants.STARTTLS);
498 
499                 mTransport.reopenTls();
500                 createParser();
501                 // Per RFC requirement (3501-6.2.1) gather new capabilities
502                 return(queryCapabilities());
503             } else {
504                 if (MailActivityEmail.DEBUG) {
505                     LogUtils.d(Logging.LOG_TAG, "TLS not supported but required");
506                 }
507                 throw new MessagingException(MessagingException.TLS_REQUIRED);
508             }
509         }
510         return null;
511     }
512 
513     /** @see DiscourseLogger#logLastDiscourse() */
logLastDiscourse()514     void logLastDiscourse() {
515         mDiscourse.logLastDiscourse();
516     }
517 }