ImapStore.java revision ba7652cda04d428ae7959f8bf6cbc6efe028bfce
1/* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.email.mail.store; 18 19import com.android.email.Email; 20import com.android.email.Preferences; 21import com.android.email.VendorPolicyLoader; 22import com.android.email.mail.Store; 23import com.android.email.mail.Transport; 24import com.android.email.mail.store.imap.ImapConstants; 25import com.android.email.mail.store.imap.ImapList; 26import com.android.email.mail.store.imap.ImapResponse; 27import com.android.email.mail.store.imap.ImapResponseParser; 28import com.android.email.mail.store.imap.ImapString; 29import com.android.email.mail.store.imap.ImapUtility; 30import com.android.email.mail.transport.DiscourseLogger; 31import com.android.email.mail.transport.MailTransport; 32import com.android.emailcommon.Logging; 33import com.android.emailcommon.internet.MimeMessage; 34import com.android.emailcommon.mail.AuthenticationFailedException; 35import com.android.emailcommon.mail.CertificateValidationException; 36import com.android.emailcommon.mail.Flag; 37import com.android.emailcommon.mail.Folder; 38import com.android.emailcommon.mail.Message; 39import com.android.emailcommon.mail.MessagingException; 40import com.android.emailcommon.service.EmailServiceProxy; 41import com.android.emailcommon.utility.Utility; 42import com.beetstra.jutf7.CharsetProvider; 43 44import android.content.Context; 45import android.os.Build; 46import android.os.Bundle; 47import android.telephony.TelephonyManager; 48import android.text.TextUtils; 49import android.util.Base64; 50import android.util.Log; 51 52import java.io.IOException; 53import java.io.InputStream; 54import java.net.URI; 55import java.net.URISyntaxException; 56import java.nio.ByteBuffer; 57import java.nio.charset.Charset; 58import java.security.MessageDigest; 59import java.security.NoSuchAlgorithmException; 60import java.util.ArrayList; 61import java.util.Collection; 62import java.util.Collections; 63import java.util.HashMap; 64import java.util.List; 65import java.util.concurrent.ConcurrentLinkedQueue; 66import java.util.concurrent.atomic.AtomicInteger; 67import java.util.regex.Pattern; 68 69import javax.net.ssl.SSLException; 70 71/** 72 * <pre> 73 * TODO Need to start keeping track of UIDVALIDITY 74 * TODO Need a default response handler for things like folder updates 75 * TODO In fetch(), if we need a ImapMessage and were given 76 * TODO Collect ALERT messages and show them to users. 77 * something else we can try to do a pre-fetch first. 78 * 79 * ftp://ftp.isi.edu/in-notes/rfc2683.txt When a client asks for 80 * certain information in a FETCH command, the server may return the requested 81 * information in any order, not necessarily in the order that it was requested. 82 * Further, the server may return the information in separate FETCH responses 83 * and may also return information that was not explicitly requested (to reflect 84 * to the client changes in the state of the subject message). 85 * </pre> 86 */ 87public class ImapStore extends Store { 88 89 // Always check in FALSE 90 private static final boolean DEBUG_FORCE_SEND_ID = false; 91 92 static final int COPY_BUFFER_SIZE = 16*1024; 93 94 static final Flag[] PERMANENT_FLAGS = { Flag.DELETED, Flag.SEEN, Flag.FLAGGED }; 95 private final Context mContext; 96 private Transport mRootTransport; 97 private String mUsername; 98 private String mPassword; 99 private String mLoginPhrase; 100 private String mIdPhrase = null; 101 private static String sImapId = null; 102 /*package*/ String mPathPrefix; 103 /*package*/ String mPathSeparator; 104 105 private final ConcurrentLinkedQueue<ImapConnection> mConnectionPool = 106 new ConcurrentLinkedQueue<ImapConnection>(); 107 108 /** 109 * Charset used for converting folder names to and from UTF-7 as defined by RFC 3501. 110 */ 111 private static final Charset MODIFIED_UTF_7_CHARSET = 112 new CharsetProvider().charsetForName("X-RFC-3501"); 113 114 /** 115 * Cache of ImapFolder objects. ImapFolders are attached to a given folder on the server 116 * and as long as their associated connection remains open they are reusable between 117 * requests. This cache lets us make sure we always reuse, if possible, for a given 118 * folder name. 119 */ 120 private HashMap<String, ImapFolder> mFolderCache = new HashMap<String, ImapFolder>(); 121 122 /** 123 * Next tag to use. All connections associated to the same ImapStore instance share the same 124 * counter to make tests simpler. 125 * (Some of the tests involve multiple connections but only have a single counter to track the 126 * tag.) 127 */ 128 private final AtomicInteger mNextCommandTag = new AtomicInteger(0); 129 130 /** 131 * Static named constructor. 132 */ 133 public static Store newInstance(String uri, Context context, PersistentDataCallbacks callbacks) 134 throws MessagingException { 135 return new ImapStore(context, uri); 136 } 137 138 /** 139 * Allowed formats for the Uri: 140 * imap://user:password@server:port 141 * imap+tls+://user:password@server:port 142 * imap+tls+trustallcerts://user:password@server:port 143 * imap+ssl+://user:password@server:port 144 * imap+ssl+trustallcerts://user:password@server:port 145 * 146 * @param uriString the Uri containing information to configure this store 147 */ 148 private ImapStore(Context context, String uriString) throws MessagingException { 149 mContext = context; 150 URI uri; 151 try { 152 uri = new URI(uriString); 153 } catch (URISyntaxException use) { 154 throw new MessagingException("Invalid ImapStore URI", use); 155 } 156 157 String scheme = uri.getScheme(); 158 if (scheme == null || !scheme.startsWith(STORE_SCHEME_IMAP)) { 159 throw new MessagingException("Unsupported protocol"); 160 } 161 // defaults, which can be changed by security modifiers 162 int connectionSecurity = Transport.CONNECTION_SECURITY_NONE; 163 int defaultPort = 143; 164 // check for security modifiers and apply changes 165 if (scheme.contains("+ssl")) { 166 connectionSecurity = Transport.CONNECTION_SECURITY_SSL; 167 defaultPort = 993; 168 } else if (scheme.contains("+tls")) { 169 connectionSecurity = Transport.CONNECTION_SECURITY_TLS; 170 } 171 boolean trustCertificates = scheme.contains(STORE_SECURITY_TRUST_CERTIFICATES); 172 173 mRootTransport = new MailTransport("IMAP"); 174 mRootTransport.setUri(uri, defaultPort); 175 mRootTransport.setSecurity(connectionSecurity, trustCertificates); 176 177 String[] userInfoParts = mRootTransport.getUserInfoParts(); 178 if (userInfoParts != null) { 179 mUsername = userInfoParts[0]; 180 if (userInfoParts.length > 1) { 181 mPassword = userInfoParts[1]; 182 183 // build the LOGIN string once (instead of over-and-over again.) 184 // apply the quoting here around the built-up password 185 mLoginPhrase = ImapConstants.LOGIN + " " + mUsername + " " 186 + ImapUtility.imapQuoted(mPassword); 187 } 188 } 189 190 if ((uri.getPath() != null) && (uri.getPath().length() > 0)) { 191 mPathPrefix = uri.getPath().substring(1); 192 } 193 } 194 195 /* package */ Collection<ImapConnection> getConnectionPoolForTest() { 196 return mConnectionPool; 197 } 198 199 /** 200 * For testing only. Injects a different root transport (it will be copied using 201 * newInstanceWithConfiguration() each time IMAP sets up a new channel). The transport 202 * should already be set up and ready to use. Do not use for real code. 203 * @param testTransport The Transport to inject and use for all future communication. 204 */ 205 /* package */ void setTransport(Transport testTransport) { 206 mRootTransport = testTransport; 207 } 208 209 /** 210 * Return, or create and return, an string suitable for use in an IMAP ID message. 211 * This is constructed similarly to the way the browser sets up its user-agent strings. 212 * See RFC 2971 for more details. The output of this command will be a series of key-value 213 * pairs delimited by spaces (there is no point in returning a structured result because 214 * this will be sent as-is to the IMAP server). No tokens, parenthesis or "ID" are included, 215 * because some connections may append additional values. 216 * 217 * The following IMAP ID keys may be included: 218 * name Android package name of the program 219 * os "android" 220 * os-version "version; model; build-id" 221 * vendor Vendor of the client/server 222 * x-android-device-model Model (only revealed if release build) 223 * x-android-net-operator Mobile network operator (if known) 224 * AGUID A device+account UID 225 * 226 * In addition, a vendor policy .apk can append key/value pairs. 227 * 228 * @param userName the username of the account 229 * @param host the host (server) of the account 230 * @param capabilities a list of the capabilities from the server 231 * @return a String for use in an IMAP ID message. 232 */ 233 /* package */ static String getImapId(Context context, String userName, String host, 234 String capabilities) { 235 // The first section is global to all IMAP connections, and generates the fixed 236 // values in any IMAP ID message 237 synchronized (ImapStore.class) { 238 if (sImapId == null) { 239 TelephonyManager tm = 240 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 241 String networkOperator = tm.getNetworkOperatorName(); 242 if (networkOperator == null) networkOperator = ""; 243 244 sImapId = makeCommonImapId(context.getPackageName(), Build.VERSION.RELEASE, 245 Build.VERSION.CODENAME, Build.MODEL, Build.ID, Build.MANUFACTURER, 246 networkOperator); 247 } 248 } 249 250 // This section is per Store, and adds in a dynamic elements like UID's. 251 // We don't cache the result of this work, because the caller does anyway. 252 StringBuilder id = new StringBuilder(sImapId); 253 254 // Optionally add any vendor-supplied id keys 255 String vendorId = VendorPolicyLoader.getInstance(context).getImapIdValues(userName, host, 256 capabilities); 257 if (vendorId != null) { 258 id.append(' '); 259 id.append(vendorId); 260 } 261 262 // Generate a UID that mixes a "stable" device UID with the email address 263 try { 264 String devUID = Preferences.getPreferences(context).getDeviceUID(); 265 MessageDigest messageDigest; 266 messageDigest = MessageDigest.getInstance("SHA-1"); 267 messageDigest.update(userName.getBytes()); 268 messageDigest.update(devUID.getBytes()); 269 byte[] uid = messageDigest.digest(); 270 String hexUid = Base64.encodeToString(uid, Base64.NO_WRAP); 271 id.append(" \"AGUID\" \""); 272 id.append(hexUid); 273 id.append('\"'); 274 } catch (NoSuchAlgorithmException e) { 275 Log.d(Logging.LOG_TAG, "couldn't obtain SHA-1 hash for device UID"); 276 } 277 return id.toString(); 278 } 279 280 /** 281 * Helper function that actually builds the static part of the IMAP ID string. This is 282 * separated from getImapId for testability. There is no escaping or encoding in IMAP ID so 283 * any rogue chars must be filtered here. 284 * 285 * @param packageName context.getPackageName() 286 * @param version Build.VERSION.RELEASE 287 * @param codeName Build.VERSION.CODENAME 288 * @param model Build.MODEL 289 * @param id Build.ID 290 * @param vendor Build.MANUFACTURER 291 * @param networkOperator TelephonyManager.getNetworkOperatorName() 292 * @return the static (never changes) portion of the IMAP ID 293 */ 294 /* package */ static String makeCommonImapId(String packageName, String version, 295 String codeName, String model, String id, String vendor, String networkOperator) { 296 297 // Before building up IMAP ID string, pre-filter the input strings for "legal" chars 298 // This is using a fairly arbitrary char set intended to pass through most reasonable 299 // version, model, and vendor strings: a-z A-Z 0-9 - _ + = ; : . , / <space> 300 // The most important thing is *not* to pass parens, quotes, or CRLF, which would break 301 // the format of the IMAP ID list. 302 Pattern p = Pattern.compile("[^a-zA-Z0-9-_\\+=;:\\.,/ ]"); 303 packageName = p.matcher(packageName).replaceAll(""); 304 version = p.matcher(version).replaceAll(""); 305 codeName = p.matcher(codeName).replaceAll(""); 306 model = p.matcher(model).replaceAll(""); 307 id = p.matcher(id).replaceAll(""); 308 vendor = p.matcher(vendor).replaceAll(""); 309 networkOperator = p.matcher(networkOperator).replaceAll(""); 310 311 // "name" "com.android.email" 312 StringBuffer sb = new StringBuffer("\"name\" \""); 313 sb.append(packageName); 314 sb.append("\""); 315 316 // "os" "android" 317 sb.append(" \"os\" \"android\""); 318 319 // "os-version" "version; build-id" 320 sb.append(" \"os-version\" \""); 321 if (version.length() > 0) { 322 sb.append(version); 323 } else { 324 // default to "1.0" 325 sb.append("1.0"); 326 } 327 // add the build ID or build # 328 if (id.length() > 0) { 329 sb.append("; "); 330 sb.append(id); 331 } 332 sb.append("\""); 333 334 // "vendor" "the vendor" 335 if (vendor.length() > 0) { 336 sb.append(" \"vendor\" \""); 337 sb.append(vendor); 338 sb.append("\""); 339 } 340 341 // "x-android-device-model" the device model (on release builds only) 342 if ("REL".equals(codeName)) { 343 if (model.length() > 0) { 344 sb.append(" \"x-android-device-model\" \""); 345 sb.append(model); 346 sb.append("\""); 347 } 348 } 349 350 // "x-android-mobile-net-operator" "name of network operator" 351 if (networkOperator.length() > 0) { 352 sb.append(" \"x-android-mobile-net-operator\" \""); 353 sb.append(networkOperator); 354 sb.append("\""); 355 } 356 357 return sb.toString(); 358 } 359 360 361 @Override 362 public Folder getFolder(String name) { 363 ImapFolder folder; 364 synchronized (mFolderCache) { 365 folder = mFolderCache.get(name); 366 if (folder == null) { 367 folder = new ImapFolder(this, name); 368 mFolderCache.put(name, folder); 369 } 370 } 371 return folder; 372 } 373 374 @Override 375 public Folder[] getAllFolders() throws MessagingException { 376 ImapConnection connection = getConnection(); 377 try { 378 ArrayList<Folder> folders = new ArrayList<Folder>(); 379 // Establish a connection to the IMAP server; if necessary 380 // This ensures a valid prefix if the prefix is automatically set by the server 381 connection.executeSimpleCommand(ImapConstants.NOOP); 382 String imapCommand = ImapConstants.LIST + " \"\" \"*\""; 383 if (mPathPrefix != null) { 384 imapCommand = ImapConstants.LIST + " \"\" \"" + mPathPrefix + "*\""; 385 } 386 List<ImapResponse> responses = connection.executeSimpleCommand(imapCommand); 387 for (ImapResponse response : responses) { 388 // S: * LIST (\Noselect) "/" ~/Mail/foo 389 if (response.isDataResponse(0, ImapConstants.LIST)) { 390 boolean includeFolder = true; 391 392 // Get folder name. 393 ImapString encodedFolder = response.getStringOrEmpty(3); 394 if (encodedFolder.isEmpty()) continue; 395 String folder = decodeFolderName(encodedFolder.getString(), mPathPrefix); 396 if (ImapConstants.INBOX.equalsIgnoreCase(folder)) { 397 continue; 398 } 399 400 // Parse attributes. 401 if (response.getListOrEmpty(1).contains(ImapConstants.FLAG_NO_SELECT)) { 402 includeFolder = false; 403 } 404 if (includeFolder) { 405 folders.add(getFolder(folder)); 406 } 407 } 408 } 409 folders.add(getFolder(ImapConstants.INBOX)); 410 return folders.toArray(new Folder[] {}); 411 } catch (IOException ioe) { 412 connection.close(); 413 throw new MessagingException("Unable to get folder list.", ioe); 414 } catch (AuthenticationFailedException afe) { 415 // We do NOT want this connection pooled, or we will continue to send NOOP and SELECT 416 // commands to the server 417 connection.destroyResponses(); 418 connection = null; 419 throw afe; 420 } finally { 421 if (connection != null) { 422 connection.destroyResponses(); 423 poolConnection(connection); 424 } 425 } 426 } 427 428 @Override 429 public Bundle checkSettings() throws MessagingException { 430 int result = MessagingException.NO_ERROR; 431 Bundle bundle = new Bundle(); 432 ImapConnection connection = new ImapConnection(); 433 try { 434 connection.open(); 435 connection.close(); 436 } catch (IOException ioe) { 437 bundle.putString(EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE, ioe.getMessage()); 438 result = MessagingException.IOERROR; 439 } finally { 440 connection.destroyResponses(); 441 } 442 bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, result); 443 return bundle; 444 } 445 446 /** 447 * Fixes the path prefix, if necessary. The path prefix must always end with the 448 * path separator. 449 */ 450 /*package*/ void ensurePrefixIsValid() { 451 // Make sure the path prefix ends with the path separator 452 if (!TextUtils.isEmpty(mPathPrefix) && !TextUtils.isEmpty(mPathSeparator)) { 453 if (!mPathPrefix.endsWith(mPathSeparator)) { 454 mPathPrefix = mPathPrefix + mPathSeparator; 455 } 456 } 457 } 458 459 /** 460 * Gets a connection if one is available from the pool, or creates a new one if not. 461 */ 462 /* package */ ImapConnection getConnection() { 463 ImapConnection connection = null; 464 while ((connection = mConnectionPool.poll()) != null) { 465 try { 466 connection.executeSimpleCommand(ImapConstants.NOOP); 467 break; 468 } catch (MessagingException e) { 469 // Fall through 470 } catch (IOException e) { 471 // Fall through 472 } finally { 473 connection.destroyResponses(); 474 } 475 connection.close(); 476 connection = null; 477 } 478 if (connection == null) { 479 connection = new ImapConnection(); 480 } 481 return connection; 482 } 483 484 /** 485 * Save a {@link ImapConnection} in the pool for reuse. 486 */ 487 /* package */ void poolConnection(ImapConnection connection) { 488 if (connection != null) { 489 mConnectionPool.add(connection); 490 } 491 } 492 493 /** 494 * Prepends the folder name with the given prefix and UTF-7 encodes it. 495 */ 496 /* package */ static String encodeFolderName(String name, String prefix) { 497 // do NOT add the prefix to the special name "INBOX" 498 if (ImapConstants.INBOX.equalsIgnoreCase(name)) return name; 499 500 // Prepend prefix 501 if (prefix != null) { 502 name = prefix + name; 503 } 504 505 // TODO bypass the conversion if name doesn't have special char. 506 ByteBuffer bb = MODIFIED_UTF_7_CHARSET.encode(name); 507 byte[] b = new byte[bb.limit()]; 508 bb.get(b); 509 510 return Utility.fromAscii(b); 511 } 512 513 /** 514 * UTF-7 decodes the folder name and removes the given path prefix. 515 */ 516 /* package */ static String decodeFolderName(String name, String prefix) { 517 // TODO bypass the conversion if name doesn't have special char. 518 String folder; 519 folder = MODIFIED_UTF_7_CHARSET.decode(ByteBuffer.wrap(Utility.toAscii(name))).toString(); 520 if ((prefix != null) && folder.startsWith(prefix)) { 521 folder = folder.substring(prefix.length()); 522 } 523 return folder; 524 } 525 526 /** 527 * Returns UIDs of Messages joined with "," as the separator. 528 */ 529 /* package */ static String joinMessageUids(Message[] messages) { 530 StringBuilder sb = new StringBuilder(); 531 boolean notFirst = false; 532 for (Message m : messages) { 533 if (notFirst) { 534 sb.append(','); 535 } 536 sb.append(m.getUid()); 537 notFirst = true; 538 } 539 return sb.toString(); 540 } 541 542 /** 543 * A cacheable class that stores the details for a single IMAP connection. 544 */ 545 class ImapConnection { 546 /** ID capability per RFC 2971*/ 547 public static final int CAPABILITY_ID = 1 << 0; 548 /** NAMESPACE capability per RFC 2342 */ 549 public static final int CAPABILITY_NAMESPACE = 1 << 1; 550 /** STARTTLS capability per RFC 3501 */ 551 public static final int CAPABILITY_STARTTLS = 1 << 2; 552 /** UIDPLUS capability per RFC 4315 */ 553 public static final int CAPABILITY_UIDPLUS = 1 << 3; 554 555 /** The capabilities supported; a set of CAPABILITY_* values. */ 556 private int mCapabilities; 557 private static final String IMAP_DEDACTED_LOG = "[IMAP command redacted]"; 558 Transport mTransport; 559 private ImapResponseParser mParser; 560 /** # of command/response lines to log upon crash. */ 561 private static final int DISCOURSE_LOGGER_SIZE = 64; 562 private final DiscourseLogger mDiscourse = new DiscourseLogger(DISCOURSE_LOGGER_SIZE); 563 564 public void open() throws IOException, MessagingException { 565 if (mTransport != null && mTransport.isOpen()) { 566 return; 567 } 568 569 try { 570 // copy configuration into a clean transport, if necessary 571 if (mTransport == null) { 572 mTransport = mRootTransport.newInstanceWithConfiguration(); 573 } 574 575 mTransport.open(); 576 mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT); 577 578 createParser(); 579 580 // BANNER 581 mParser.readResponse(); 582 583 // CAPABILITY 584 ImapResponse capabilities = queryCapabilities(); 585 586 boolean hasStartTlsCapability = 587 capabilities.contains(ImapConstants.STARTTLS); 588 589 // TLS 590 ImapResponse newCapabilities = doStartTls(hasStartTlsCapability); 591 if (newCapabilities != null) { 592 capabilities = newCapabilities; 593 } 594 595 // NOTE: An IMAP response MUST be processed before issuing any new IMAP 596 // requests. Subsequent requests may destroy previous response data. As 597 // such, we save away capability information here for future use. 598 setCapabilities(capabilities); 599 String capabilityString = capabilities.flatten(); 600 601 // ID 602 doSendId(isCapable(CAPABILITY_ID), capabilityString); 603 604 // LOGIN 605 doLogin(); 606 607 // NAMESPACE (only valid in the Authenticated state) 608 doGetNamespace(isCapable(CAPABILITY_NAMESPACE)); 609 610 // Gets the path separator from the server 611 doGetPathSeparator(); 612 613 ensurePrefixIsValid(); 614 } catch (SSLException e) { 615 if (Email.DEBUG) { 616 Log.d(Logging.LOG_TAG, e.toString()); 617 } 618 throw new CertificateValidationException(e.getMessage(), e); 619 } catch (IOException ioe) { 620 // NOTE: Unlike similar code in POP3, I'm going to rethrow as-is. There is a lot 621 // of other code here that catches IOException and I don't want to break it. 622 // This catch is only here to enhance logging of connection-time issues. 623 if (Email.DEBUG) { 624 Log.d(Logging.LOG_TAG, ioe.toString()); 625 } 626 throw ioe; 627 } finally { 628 destroyResponses(); 629 } 630 } 631 632 public void close() { 633 if (mTransport != null) { 634 mTransport.close(); 635 mTransport = null; 636 } 637 } 638 639 /** 640 * Returns whether or not the specified capability is supported by the server. 641 */ 642 public boolean isCapable(int capability) { 643 return (mCapabilities & capability) != 0; 644 } 645 646 /** 647 * Sets the capability flags according to the response provided by the server. 648 * Note: We only set the capability flags that we are interested in. There are many IMAP 649 * capabilities that we do not track. 650 */ 651 private void setCapabilities(ImapResponse capabilities) { 652 if (capabilities.contains(ImapConstants.ID)) { 653 mCapabilities |= CAPABILITY_ID; 654 } 655 if (capabilities.contains(ImapConstants.NAMESPACE)) { 656 mCapabilities |= CAPABILITY_NAMESPACE; 657 } 658 if (capabilities.contains(ImapConstants.UIDPLUS)) { 659 mCapabilities |= CAPABILITY_UIDPLUS; 660 } 661 if (capabilities.contains(ImapConstants.STARTTLS)) { 662 mCapabilities |= CAPABILITY_STARTTLS; 663 } 664 } 665 666 /** 667 * Create an {@link ImapResponseParser} from {@code mTransport.getInputStream()} and 668 * set it to {@link #mParser}. 669 * 670 * If we already have an {@link ImapResponseParser}, we 671 * {@link #destroyResponses()} and throw it away. 672 */ 673 private void createParser() { 674 destroyResponses(); 675 mParser = new ImapResponseParser(mTransport.getInputStream(), mDiscourse); 676 } 677 678 public void destroyResponses() { 679 if (mParser != null) { 680 mParser.destroyResponses(); 681 } 682 } 683 684 /* package */ boolean isTransportOpenForTest() { 685 return mTransport != null ? mTransport.isOpen() : false; 686 } 687 688 public ImapResponse readResponse() throws IOException, MessagingException { 689 return mParser.readResponse(); 690 } 691 692 /** 693 * Send a single command to the server. The command will be preceded by an IMAP command 694 * tag and followed by \r\n (caller need not supply them). 695 * 696 * @param command The command to send to the server 697 * @param sensitive If true, the command will not be logged 698 * @return Returns the command tag that was sent 699 */ 700 public String sendCommand(String command, boolean sensitive) 701 throws MessagingException, IOException { 702 open(); 703 String tag = Integer.toString(mNextCommandTag.incrementAndGet()); 704 String commandToSend = tag + " " + command; 705 mTransport.writeLine(commandToSend, sensitive ? IMAP_DEDACTED_LOG : null); 706 mDiscourse.addSentCommand(sensitive ? IMAP_DEDACTED_LOG : commandToSend); 707 return tag; 708 } 709 710 /*package*/ List<ImapResponse> executeSimpleCommand(String command) throws IOException, 711 MessagingException { 712 return executeSimpleCommand(command, false); 713 } 714 715 /*package*/ List<ImapResponse> executeSimpleCommand(String command, boolean sensitive) 716 throws IOException, MessagingException { 717 String tag = sendCommand(command, sensitive); 718 ArrayList<ImapResponse> responses = new ArrayList<ImapResponse>(); 719 ImapResponse response; 720 do { 721 response = mParser.readResponse(); 722 responses.add(response); 723 } while (!response.isTagged()); 724 if (!response.isOk()) { 725 final String toString = response.toString(); 726 final String alert = response.getAlertTextOrEmpty().getString(); 727 destroyResponses(); 728 throw new ImapException(toString, alert); 729 } 730 return responses; 731 } 732 733 /** 734 * Query server for capabilities. 735 */ 736 private ImapResponse queryCapabilities() throws IOException, MessagingException { 737 ImapResponse capabilityResponse = null; 738 for (ImapResponse r : executeSimpleCommand(ImapConstants.CAPABILITY)) { 739 if (r.is(0, ImapConstants.CAPABILITY)) { 740 capabilityResponse = r; 741 break; 742 } 743 } 744 if (capabilityResponse == null) { 745 throw new MessagingException("Invalid CAPABILITY response received"); 746 } 747 return capabilityResponse; 748 } 749 750 /** 751 * Sends client identification information to the IMAP server per RFC 2971. If 752 * the server does not support the ID command, this will perform no operation. 753 * 754 * Interoperability hack: Never send ID to *.secureserver.net, which sends back a 755 * malformed response that our parser can't deal with. 756 */ 757 private void doSendId(boolean hasIdCapability, String capabilities) 758 throws MessagingException { 759 if (!hasIdCapability) return; 760 761 // Never send ID to *.secureserver.net 762 String host = mRootTransport.getHost(); 763 if (host.toLowerCase().endsWith(".secureserver.net")) return; 764 765 // Assign user-agent string (for RFC2971 ID command) 766 String mUserAgent = getImapId(mContext, mUsername, host, capabilities); 767 768 if (mUserAgent != null) { 769 mIdPhrase = ImapConstants.ID + " (" + mUserAgent + ")"; 770 } else if (DEBUG_FORCE_SEND_ID) { 771 mIdPhrase = ImapConstants.ID + " " + ImapConstants.NIL; 772 } 773 // else: mIdPhrase = null, no ID will be emitted 774 775 // Send user-agent in an RFC2971 ID command 776 if (mIdPhrase != null) { 777 try { 778 executeSimpleCommand(mIdPhrase); 779 } catch (ImapException ie) { 780 // Log for debugging, but this is not a fatal problem. 781 if (Email.DEBUG) { 782 Log.d(Logging.LOG_TAG, ie.toString()); 783 } 784 } catch (IOException ioe) { 785 // Special case to handle malformed OK responses and ignore them. 786 // A true IOException will recur on the following login steps 787 // This can go away after the parser is fixed - see bug 2138981 788 } 789 } 790 } 791 792 /** 793 * Gets the user's Personal Namespace from the IMAP server per RFC 2342. If the user 794 * explicitly sets a namespace (using setup UI) or if the server does not support the 795 * namespace command, this will perform no operation. 796 */ 797 private void doGetNamespace(boolean hasNamespaceCapability) throws MessagingException { 798 // user did not specify a hard-coded prefix; try to get it from the server 799 if (hasNamespaceCapability && TextUtils.isEmpty(mPathPrefix)) { 800 List<ImapResponse> responseList = Collections.emptyList(); 801 802 try { 803 responseList = executeSimpleCommand(ImapConstants.NAMESPACE); 804 } catch (ImapException ie) { 805 // Log for debugging, but this is not a fatal problem. 806 if (Email.DEBUG) { 807 Log.d(Logging.LOG_TAG, ie.toString()); 808 } 809 } catch (IOException ioe) { 810 // Special case to handle malformed OK responses and ignore them. 811 } 812 813 for (ImapResponse response: responseList) { 814 if (response.isDataResponse(0, ImapConstants.NAMESPACE)) { 815 ImapList namespaceList = response.getListOrEmpty(1); 816 ImapList namespace = namespaceList.getListOrEmpty(0); 817 String namespaceString = namespace.getStringOrEmpty(0).getString(); 818 if (!TextUtils.isEmpty(namespaceString)) { 819 mPathPrefix = decodeFolderName(namespaceString, null); 820 mPathSeparator = namespace.getStringOrEmpty(1).getString(); 821 } 822 } 823 } 824 } 825 } 826 827 /** 828 * Logs into the IMAP server 829 */ 830 private void doLogin() 831 throws IOException, MessagingException, AuthenticationFailedException { 832 try { 833 // TODO eventually we need to add additional authentication 834 // options such as SASL 835 executeSimpleCommand(mLoginPhrase, true); 836 } catch (ImapException ie) { 837 if (Email.DEBUG) { 838 Log.d(Logging.LOG_TAG, ie.toString()); 839 } 840 throw new AuthenticationFailedException(ie.getAlertText(), ie); 841 842 } catch (MessagingException me) { 843 throw new AuthenticationFailedException(null, me); 844 } 845 } 846 847 /** 848 * Gets the path separator per the LIST command in RFC 3501. If the path separator 849 * was obtained while obtaining the namespace or there is no prefix defined, this 850 * will perform no operation. 851 */ 852 private void doGetPathSeparator() throws MessagingException { 853 // user did not specify a hard-coded prefix; try to get it from the server 854 if (TextUtils.isEmpty(mPathSeparator) && !TextUtils.isEmpty(mPathPrefix)) { 855 List<ImapResponse> responseList = Collections.emptyList(); 856 857 try { 858 responseList = executeSimpleCommand(ImapConstants.LIST + " \"\" \"\""); 859 } catch (ImapException ie) { 860 // Log for debugging, but this is not a fatal problem. 861 if (Email.DEBUG) { 862 Log.d(Logging.LOG_TAG, ie.toString()); 863 } 864 } catch (IOException ioe) { 865 // Special case to handle malformed OK responses and ignore them. 866 } 867 868 for (ImapResponse response: responseList) { 869 if (response.isDataResponse(0, ImapConstants.LIST)) { 870 mPathSeparator = response.getStringOrEmpty(2).getString(); 871 } 872 } 873 } 874 } 875 876 /** 877 * Starts a TLS session with the IMAP server per RFC 3501. If the user has not opted 878 * to use TLS or the server does not support the TLS capability, this will perform 879 * no operation. 880 */ 881 private ImapResponse doStartTls(boolean hasStartTlsCapability) 882 throws IOException, MessagingException { 883 if (mTransport.canTryTlsSecurity()) { 884 if (hasStartTlsCapability) { 885 // STARTTLS 886 executeSimpleCommand(ImapConstants.STARTTLS); 887 888 mTransport.reopenTls(); 889 mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT); 890 createParser(); 891 // Per RFC requirement (3501-6.2.1) gather new capabilities 892 return(queryCapabilities()); 893 } else { 894 if (Email.DEBUG) { 895 Log.d(Logging.LOG_TAG, "TLS not supported but required"); 896 } 897 throw new MessagingException(MessagingException.TLS_REQUIRED); 898 } 899 } 900 return null; 901 } 902 903 /** @see DiscourseLogger#logLastDiscourse() */ 904 public void logLastDiscourse() { 905 mDiscourse.logLastDiscourse(); 906 } 907 } 908 909 static class ImapMessage extends MimeMessage { 910 ImapMessage(String uid, ImapFolder folder) { 911 this.mUid = uid; 912 this.mFolder = folder; 913 } 914 915 public void setSize(int size) { 916 this.mSize = size; 917 } 918 919 @Override 920 public void parse(InputStream in) throws IOException, MessagingException { 921 super.parse(in); 922 } 923 924 public void setFlagInternal(Flag flag, boolean set) throws MessagingException { 925 super.setFlag(flag, set); 926 } 927 928 @Override 929 public void setFlag(Flag flag, boolean set) throws MessagingException { 930 super.setFlag(flag, set); 931 mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set); 932 } 933 } 934 935 static class ImapException extends MessagingException { 936 private static final long serialVersionUID = 1L; 937 938 String mAlertText; 939 940 public ImapException(String message, String alertText, Throwable throwable) { 941 super(message, throwable); 942 this.mAlertText = alertText; 943 } 944 945 public ImapException(String message, String alertText) { 946 super(message); 947 this.mAlertText = alertText; 948 } 949 950 public String getAlertText() { 951 return mAlertText; 952 } 953 954 public void setAlertText(String alertText) { 955 mAlertText = alertText; 956 } 957 } 958} 959