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 82 ImapConnection(ImapStore store, String username, String password) { 83 setStore(store, username, password); 84 } 85 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 } 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 */ 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 */ 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 */ 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 */ 212 private void createParser() { 213 destroyResponses(); 214 mParser = new ImapResponseParser(mTransport.getInputStream(), mDiscourse); 215 } 216 217 void destroyResponses() { 218 if (mParser != null) { 219 mParser.destroyResponses(); 220 } 221 } 222 223 boolean isTransportOpenForTest() { 224 return mTransport != null ? mTransport.isOpen() : false; 225 } 226 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 */ 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 */ 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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() */ 514 void logLastDiscourse() { 515 mDiscourse.logLastDiscourse(); 516 } 517 }