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