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.Utility; 22import com.android.email.VendorPolicyLoader; 23import com.android.email.mail.AuthenticationFailedException; 24import com.android.email.mail.CertificateValidationException; 25import com.android.email.mail.FetchProfile; 26import com.android.email.mail.Flag; 27import com.android.email.mail.Folder; 28import com.android.email.mail.Message; 29import com.android.email.mail.MessagingException; 30import com.android.email.mail.Part; 31import com.android.email.mail.Store; 32import com.android.email.mail.Transport; 33import com.android.email.mail.internet.MimeBodyPart; 34import com.android.email.mail.internet.MimeHeader; 35import com.android.email.mail.internet.MimeMessage; 36import com.android.email.mail.internet.MimeMultipart; 37import com.android.email.mail.internet.MimeUtility; 38import com.android.email.mail.store.imap.ImapConstants; 39import com.android.email.mail.store.imap.ImapElement; 40import com.android.email.mail.store.imap.ImapList; 41import com.android.email.mail.store.imap.ImapResponse; 42import com.android.email.mail.store.imap.ImapResponseParser; 43import com.android.email.mail.store.imap.ImapString; 44import com.android.email.mail.transport.CountingOutputStream; 45import com.android.email.mail.transport.DiscourseLogger; 46import com.android.email.mail.transport.EOLConvertingOutputStream; 47import com.android.email.mail.transport.MailTransport; 48import com.beetstra.jutf7.CharsetProvider; 49 50import android.content.Context; 51import android.os.Build; 52import android.telephony.TelephonyManager; 53import android.text.TextUtils; 54import android.util.Base64; 55import android.util.Config; 56import android.util.Log; 57 58import java.io.IOException; 59import java.io.InputStream; 60import java.net.URI; 61import java.net.URISyntaxException; 62import java.nio.ByteBuffer; 63import java.nio.charset.Charset; 64import java.security.MessageDigest; 65import java.security.NoSuchAlgorithmException; 66import java.util.ArrayList; 67import java.util.Collection; 68import java.util.Date; 69import java.util.HashMap; 70import java.util.LinkedHashSet; 71import java.util.List; 72import java.util.concurrent.ConcurrentLinkedQueue; 73import java.util.concurrent.atomic.AtomicInteger; 74import java.util.regex.Pattern; 75 76import javax.net.ssl.SSLException; 77 78/** 79 * <pre> 80 * TODO Need to start keeping track of UIDVALIDITY 81 * TODO Need a default response handler for things like folder updates 82 * TODO In fetch(), if we need a ImapMessage and were given 83 * TODO Collect ALERT messages and show them to users. 84 * something else we can try to do a pre-fetch first. 85 * 86 * ftp://ftp.isi.edu/in-notes/rfc2683.txt When a client asks for 87 * certain information in a FETCH command, the server may return the requested 88 * information in any order, not necessarily in the order that it was requested. 89 * Further, the server may return the information in separate FETCH responses 90 * and may also return information that was not explicitly requested (to reflect 91 * to the client changes in the state of the subject message). 92 * </pre> 93 */ 94public class ImapStore extends Store { 95 96 // Always check in FALSE 97 private static final boolean DEBUG_FORCE_SEND_ID = false; 98 99 private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED, Flag.SEEN, Flag.FLAGGED }; 100 101 private final Context mContext; 102 private Transport mRootTransport; 103 private String mUsername; 104 private String mPassword; 105 private String mLoginPhrase; 106 private String mPathPrefix; 107 private String mIdPhrase = null; 108 private static String sImapId = null; 109 110 private final ConcurrentLinkedQueue<ImapConnection> mConnectionPool = 111 new ConcurrentLinkedQueue<ImapConnection>(); 112 113 /** 114 * Charset used for converting folder names to and from UTF-7 as defined by RFC 3501. 115 */ 116 private static final Charset MODIFIED_UTF_7_CHARSET = 117 new CharsetProvider().charsetForName("X-RFC-3501"); 118 119 /** 120 * Cache of ImapFolder objects. ImapFolders are attached to a given folder on the server 121 * and as long as their associated connection remains open they are reusable between 122 * requests. This cache lets us make sure we always reuse, if possible, for a given 123 * folder name. 124 */ 125 private HashMap<String, ImapFolder> mFolderCache = new HashMap<String, ImapFolder>(); 126 127 /** 128 * Next tag to use. All connections associated to the same ImapStore instance share the same 129 * counter to make tests simpler. 130 * (Some of the tests involve multiple connections but only have a single counter to track the 131 * tag.) 132 */ 133 private final AtomicInteger mNextCommandTag = new AtomicInteger(0); 134 135 /** 136 * Static named constructor. 137 */ 138 public static Store newInstance(String uri, Context context, PersistentDataCallbacks callbacks) 139 throws MessagingException { 140 return new ImapStore(context, uri); 141 } 142 143 /** 144 * Allowed formats for the Uri: 145 * imap://user:password@server:port 146 * imap+tls+://user:password@server:port 147 * imap+tls+trustallcerts://user:password@server:port 148 * imap+ssl+://user:password@server:port 149 * imap+ssl+trustallcerts://user:password@server:port 150 * 151 * @param uriString the Uri containing information to configure this store 152 */ 153 private ImapStore(Context context, String uriString) throws MessagingException { 154 mContext = context; 155 URI uri; 156 try { 157 uri = new URI(uriString); 158 } catch (URISyntaxException use) { 159 throw new MessagingException("Invalid ImapStore URI", use); 160 } 161 162 String scheme = uri.getScheme(); 163 if (scheme == null || !scheme.startsWith(STORE_SCHEME_IMAP)) { 164 throw new MessagingException("Unsupported protocol"); 165 } 166 // defaults, which can be changed by security modifiers 167 int connectionSecurity = Transport.CONNECTION_SECURITY_NONE; 168 int defaultPort = 143; 169 // check for security modifiers and apply changes 170 if (scheme.contains("+ssl")) { 171 connectionSecurity = Transport.CONNECTION_SECURITY_SSL; 172 defaultPort = 993; 173 } else if (scheme.contains("+tls")) { 174 connectionSecurity = Transport.CONNECTION_SECURITY_TLS; 175 } 176 boolean trustCertificates = scheme.contains(STORE_SECURITY_TRUST_CERTIFICATES); 177 178 mRootTransport = new MailTransport("IMAP"); 179 mRootTransport.setUri(uri, defaultPort); 180 mRootTransport.setSecurity(connectionSecurity, trustCertificates); 181 182 String[] userInfoParts = mRootTransport.getUserInfoParts(); 183 if (userInfoParts != null) { 184 mUsername = userInfoParts[0]; 185 if (userInfoParts.length > 1) { 186 mPassword = userInfoParts[1]; 187 188 // build the LOGIN string once (instead of over-and-over again.) 189 // apply the quoting here around the built-up password 190 mLoginPhrase = ImapConstants.LOGIN + " " + mUsername + " " 191 + Utility.imapQuoted(mPassword); 192 } 193 } 194 195 if ((uri.getPath() != null) && (uri.getPath().length() > 0)) { 196 mPathPrefix = uri.getPath().substring(1); 197 } 198 } 199 200 /* package */ Collection<ImapConnection> getConnectionPoolForTest() { 201 return mConnectionPool; 202 } 203 204 /** 205 * For testing only. Injects a different root transport (it will be copied using 206 * newInstanceWithConfiguration() each time IMAP sets up a new channel). The transport 207 * should already be set up and ready to use. Do not use for real code. 208 * @param testTransport The Transport to inject and use for all future communication. 209 */ 210 /* package */ void setTransport(Transport testTransport) { 211 mRootTransport = testTransport; 212 } 213 214 /** 215 * Return, or create and return, an string suitable for use in an IMAP ID message. 216 * This is constructed similarly to the way the browser sets up its user-agent strings. 217 * See RFC 2971 for more details. The output of this command will be a series of key-value 218 * pairs delimited by spaces (there is no point in returning a structured result because 219 * this will be sent as-is to the IMAP server). No tokens, parenthesis or "ID" are included, 220 * because some connections may append additional values. 221 * 222 * The following IMAP ID keys may be included: 223 * name Android package name of the program 224 * os "android" 225 * os-version "version; model; build-id" 226 * vendor Vendor of the client/server 227 * x-android-device-model Model (only revealed if release build) 228 * x-android-net-operator Mobile network operator (if known) 229 * AGUID A device+account UID 230 * 231 * In addition, a vendor policy .apk can append key/value pairs. 232 * 233 * @param userName the username of the account 234 * @param host the host (server) of the account 235 * @param capabilityResponse the capabilities list from the server 236 * @return a String for use in an IMAP ID message. 237 */ 238 /* package */ static String getImapId(Context context, String userName, String host, 239 ImapResponse capabilityResponse) { 240 // The first section is global to all IMAP connections, and generates the fixed 241 // values in any IMAP ID message 242 synchronized (ImapStore.class) { 243 if (sImapId == null) { 244 TelephonyManager tm = 245 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 246 String networkOperator = tm.getNetworkOperatorName(); 247 if (networkOperator == null) networkOperator = ""; 248 249 sImapId = makeCommonImapId(context.getPackageName(), Build.VERSION.RELEASE, 250 Build.VERSION.CODENAME, Build.MODEL, Build.ID, Build.MANUFACTURER, 251 networkOperator); 252 } 253 } 254 255 // This section is per Store, and adds in a dynamic elements like UID's. 256 // We don't cache the result of this work, because the caller does anyway. 257 StringBuilder id = new StringBuilder(sImapId); 258 259 // Optionally add any vendor-supplied id keys 260 String vendorId = VendorPolicyLoader.getInstance(context).getImapIdValues(userName, host, 261 capabilityResponse.flatten()); 262 if (vendorId != null) { 263 id.append(' '); 264 id.append(vendorId); 265 } 266 267 // Generate a UID that mixes a "stable" device UID with the email address 268 try { 269 String devUID = Preferences.getPreferences(context).getDeviceUID(); 270 MessageDigest messageDigest; 271 messageDigest = MessageDigest.getInstance("SHA-1"); 272 messageDigest.update(userName.getBytes()); 273 messageDigest.update(devUID.getBytes()); 274 byte[] uid = messageDigest.digest(); 275 String hexUid = Base64.encodeToString(uid, Base64.NO_WRAP); 276 id.append(" \"AGUID\" \""); 277 id.append(hexUid); 278 id.append('\"'); 279 } catch (NoSuchAlgorithmException e) { 280 Log.d(Email.LOG_TAG, "couldn't obtain SHA-1 hash for device UID"); 281 } 282 return id.toString(); 283 } 284 285 /** 286 * Helper function that actually builds the static part of the IMAP ID string. This is 287 * separated from getImapId for testability. There is no escaping or encoding in IMAP ID so 288 * any rogue chars must be filtered here. 289 * 290 * @param packageName context.getPackageName() 291 * @param version Build.VERSION.RELEASE 292 * @param codeName Build.VERSION.CODENAME 293 * @param model Build.MODEL 294 * @param id Build.ID 295 * @param vendor Build.MANUFACTURER 296 * @param networkOperator TelephonyManager.getNetworkOperatorName() 297 * @return the static (never changes) portion of the IMAP ID 298 */ 299 /* package */ static String makeCommonImapId(String packageName, String version, 300 String codeName, String model, String id, String vendor, String networkOperator) { 301 302 // Before building up IMAP ID string, pre-filter the input strings for "legal" chars 303 // This is using a fairly arbitrary char set intended to pass through most reasonable 304 // version, model, and vendor strings: a-z A-Z 0-9 - _ + = ; : . , / <space> 305 // The most important thing is *not* to pass parens, quotes, or CRLF, which would break 306 // the format of the IMAP ID list. 307 Pattern p = Pattern.compile("[^a-zA-Z0-9-_\\+=;:\\.,/ ]"); 308 packageName = p.matcher(packageName).replaceAll(""); 309 version = p.matcher(version).replaceAll(""); 310 codeName = p.matcher(codeName).replaceAll(""); 311 model = p.matcher(model).replaceAll(""); 312 id = p.matcher(id).replaceAll(""); 313 vendor = p.matcher(vendor).replaceAll(""); 314 networkOperator = p.matcher(networkOperator).replaceAll(""); 315 316 // "name" "com.android.email" 317 StringBuffer sb = new StringBuffer("\"name\" \""); 318 sb.append(packageName); 319 sb.append("\""); 320 321 // "os" "android" 322 sb.append(" \"os\" \"android\""); 323 324 // "os-version" "version; build-id" 325 sb.append(" \"os-version\" \""); 326 if (version.length() > 0) { 327 sb.append(version); 328 } else { 329 // default to "1.0" 330 sb.append("1.0"); 331 } 332 // add the build ID or build # 333 if (id.length() > 0) { 334 sb.append("; "); 335 sb.append(id); 336 } 337 sb.append("\""); 338 339 // "vendor" "the vendor" 340 if (vendor.length() > 0) { 341 sb.append(" \"vendor\" \""); 342 sb.append(vendor); 343 sb.append("\""); 344 } 345 346 // "x-android-device-model" the device model (on release builds only) 347 if ("REL".equals(codeName)) { 348 if (model.length() > 0) { 349 sb.append(" \"x-android-device-model\" \""); 350 sb.append(model); 351 sb.append("\""); 352 } 353 } 354 355 // "x-android-mobile-net-operator" "name of network operator" 356 if (networkOperator.length() > 0) { 357 sb.append(" \"x-android-mobile-net-operator\" \""); 358 sb.append(networkOperator); 359 sb.append("\""); 360 } 361 362 return sb.toString(); 363 } 364 365 366 @Override 367 public Folder getFolder(String name) throws MessagingException { 368 ImapFolder folder; 369 synchronized (mFolderCache) { 370 folder = mFolderCache.get(name); 371 if (folder == null) { 372 folder = new ImapFolder(this, name); 373 mFolderCache.put(name, folder); 374 } 375 } 376 return folder; 377 } 378 379 @Override 380 public Folder[] getPersonalNamespaces() throws MessagingException { 381 ImapConnection connection = getConnection(); 382 try { 383 ArrayList<Folder> folders = new ArrayList<Folder>(); 384 List<ImapResponse> responses = connection.executeSimpleCommand( 385 String.format(ImapConstants.LIST + " \"\" \"%s*\"", 386 mPathPrefix == null ? "" : mPathPrefix)); 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()); 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 } finally { 415 connection.destroyResponses(); 416 poolConnection(connection); 417 } 418 } 419 420 @Override 421 public void checkSettings() throws MessagingException { 422 ImapConnection connection = new ImapConnection(); 423 try { 424 connection.open(); 425 connection.close(); 426 } catch (IOException ioe) { 427 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 428 } finally { 429 connection.destroyResponses(); 430 } 431 } 432 433 /** 434 * Gets a connection if one is available from the pool, or creates a new one if not. 435 */ 436 /* package */ ImapConnection getConnection() { 437 ImapConnection connection = null; 438 while ((connection = mConnectionPool.poll()) != null) { 439 try { 440 connection.executeSimpleCommand(ImapConstants.NOOP); 441 break; 442 } catch (MessagingException e) { 443 // Fall through 444 } catch (IOException e) { 445 // Fall through 446 } finally { 447 connection.destroyResponses(); 448 } 449 connection.close(); 450 connection = null; 451 } 452 if (connection == null) { 453 connection = new ImapConnection(); 454 } 455 return connection; 456 } 457 458 /** 459 * Save a {@link ImapConnection} in the pool for reuse. 460 */ 461 /* package */ void poolConnection(ImapConnection connection) { 462 if (connection != null) { 463 mConnectionPool.add(connection); 464 } 465 } 466 467 /* package */ static String encodeFolderName(String name) { 468 // TODO bypass the conversion if name doesn't have special char. 469 ByteBuffer bb = MODIFIED_UTF_7_CHARSET.encode(name); 470 byte[] b = new byte[bb.limit()]; 471 bb.get(b); 472 return Utility.fromAscii(b); 473 } 474 475 /* package */ static String decodeFolderName(String name) { 476 // TODO bypass the conversion if name doesn't have special char. 477 /* 478 * Convert the encoded name to US-ASCII, then pass it through the modified UTF-7 479 * decoder and return the Unicode String. 480 */ 481 return MODIFIED_UTF_7_CHARSET.decode(ByteBuffer.wrap(Utility.toAscii(name))).toString(); 482 } 483 484 /** 485 * Returns UIDs of Messages joined with "," as the separator. 486 */ 487 /* package */ static String joinMessageUids(Message[] messages) { 488 StringBuilder sb = new StringBuilder(); 489 boolean notFirst = false; 490 for (Message m : messages) { 491 if (notFirst) { 492 sb.append(','); 493 } 494 sb.append(m.getUid()); 495 notFirst = true; 496 } 497 return sb.toString(); 498 } 499 500 static class ImapFolder extends Folder { 501 private final ImapStore mStore; 502 private final String mName; 503 private int mMessageCount = -1; 504 private ImapConnection mConnection; 505 private OpenMode mMode; 506 private boolean mExists; 507 508 public ImapFolder(ImapStore store, String name) { 509 mStore = store; 510 mName = name; 511 } 512 513 private void destroyResponses() { 514 if (mConnection != null) { 515 mConnection.destroyResponses(); 516 } 517 } 518 519 @Override 520 public void open(OpenMode mode, PersistentDataCallbacks callbacks) 521 throws MessagingException { 522 try { 523 if (isOpen()) { 524 if (mMode == mode) { 525 // Make sure the connection is valid. 526 // If it's not we'll close it down and continue on to get a new one. 527 try { 528 mConnection.executeSimpleCommand(ImapConstants.NOOP); 529 return; 530 531 } catch (IOException ioe) { 532 ioExceptionHandler(mConnection, ioe); 533 } finally { 534 destroyResponses(); 535 } 536 } else { 537 // Return the connection to the pool, if exists. 538 close(false); 539 } 540 } 541 synchronized (this) { 542 mConnection = mStore.getConnection(); 543 } 544 // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft NonJunk 545 // $MDNSent) 546 // * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft 547 // NonJunk $MDNSent \*)] Flags permitted. 548 // * 23 EXISTS 549 // * 0 RECENT 550 // * OK [UIDVALIDITY 1125022061] UIDs valid 551 // * OK [UIDNEXT 57576] Predicted next UID 552 // 2 OK [READ-WRITE] Select completed. 553 try { 554 List<ImapResponse> responses = mConnection.executeSimpleCommand( 555 String.format(ImapConstants.SELECT + " \"%s\"", 556 encodeFolderName(mName))); 557 /* 558 * If the command succeeds we expect the folder has been opened read-write 559 * unless we are notified otherwise in the responses. 560 */ 561 mMode = OpenMode.READ_WRITE; 562 563 int messageCount = -1; 564 for (ImapResponse response : responses) { 565 if (response.isDataResponse(1, ImapConstants.EXISTS)) { 566 messageCount = response.getStringOrEmpty(0).getNumberOrZero(); 567 568 } else if (response.isOk()) { 569 final ImapString responseCode = response.getResponseCodeOrEmpty(); 570 if (responseCode.is(ImapConstants.READ_ONLY)) { 571 mMode = OpenMode.READ_ONLY; 572 } else if (responseCode.is(ImapConstants.READ_WRITE)) { 573 mMode = OpenMode.READ_WRITE; 574 } 575 } else if (response.isTagged()) { // Not OK 576 throw new MessagingException("Can't open mailbox: " 577 + response.getStatusResponseTextOrEmpty()); 578 } 579 } 580 581 if (messageCount == -1) { 582 throw new MessagingException("Did not find message count during select"); 583 } 584 mMessageCount = messageCount; 585 mExists = true; 586 587 } catch (IOException ioe) { 588 throw ioExceptionHandler(mConnection, ioe); 589 } finally { 590 destroyResponses(); 591 } 592 } catch (MessagingException e) { 593 mExists = false; 594 close(false); 595 throw e; 596 } 597 } 598 599 @Override 600 public boolean isOpen() { 601 return mExists && mConnection != null; 602 } 603 604 @Override 605 public OpenMode getMode() throws MessagingException { 606 return mMode; 607 } 608 609 @Override 610 public void close(boolean expunge) { 611 // TODO implement expunge 612 mMessageCount = -1; 613 synchronized (this) { 614 destroyResponses(); 615 mStore.poolConnection(mConnection); 616 mConnection = null; 617 } 618 } 619 620 @Override 621 public String getName() { 622 return mName; 623 } 624 625 @Override 626 public boolean exists() throws MessagingException { 627 if (mExists) { 628 return true; 629 } 630 /* 631 * This method needs to operate in the unselected mode as well as the selected mode 632 * so we must get the connection ourselves if it's not there. We are specifically 633 * not calling checkOpen() since we don't care if the folder is open. 634 */ 635 ImapConnection connection = null; 636 synchronized(this) { 637 if (mConnection == null) { 638 connection = mStore.getConnection(); 639 } else { 640 connection = mConnection; 641 } 642 } 643 try { 644 connection.executeSimpleCommand(String.format( 645 ImapConstants.STATUS + " \"%s\" (" + ImapConstants.UIDVALIDITY + ")", 646 encodeFolderName(mName))); 647 mExists = true; 648 return true; 649 650 } catch (MessagingException me) { 651 return false; 652 653 } catch (IOException ioe) { 654 throw ioExceptionHandler(connection, ioe); 655 656 } finally { 657 connection.destroyResponses(); 658 if (mConnection == null) { 659 mStore.poolConnection(connection); 660 } 661 } 662 } 663 664 // IMAP supports folder creation 665 @Override 666 public boolean canCreate(FolderType type) { 667 return true; 668 } 669 670 @Override 671 public boolean create(FolderType type) throws MessagingException { 672 /* 673 * This method needs to operate in the unselected mode as well as the selected mode 674 * so we must get the connection ourselves if it's not there. We are specifically 675 * not calling checkOpen() since we don't care if the folder is open. 676 */ 677 ImapConnection connection = null; 678 synchronized(this) { 679 if (mConnection == null) { 680 connection = mStore.getConnection(); 681 } else { 682 connection = mConnection; 683 } 684 } 685 try { 686 connection.executeSimpleCommand(String.format(ImapConstants.CREATE + " \"%s\"", 687 encodeFolderName(mName))); 688 return true; 689 690 } catch (MessagingException me) { 691 return false; 692 693 } catch (IOException ioe) { 694 throw ioExceptionHandler(connection, ioe); 695 696 } finally { 697 connection.destroyResponses(); 698 if (mConnection == null) { 699 mStore.poolConnection(connection); 700 } 701 } 702 } 703 704 @Override 705 public void copyMessages(Message[] messages, Folder folder, 706 MessageUpdateCallbacks callbacks) throws MessagingException { 707 checkOpen(); 708 try { 709 mConnection.executeSimpleCommand( 710 String.format(ImapConstants.UID_COPY + " %s \"%s\"", 711 joinMessageUids(messages), 712 encodeFolderName(folder.getName()))); 713 } catch (IOException ioe) { 714 throw ioExceptionHandler(mConnection, ioe); 715 } finally { 716 destroyResponses(); 717 } 718 } 719 720 @Override 721 public int getMessageCount() { 722 return mMessageCount; 723 } 724 725 @Override 726 public int getUnreadMessageCount() throws MessagingException { 727 checkOpen(); 728 try { 729 int unreadMessageCount = 0; 730 List<ImapResponse> responses = mConnection.executeSimpleCommand(String.format( 731 ImapConstants.STATUS + " \"%s\" (" + ImapConstants.UNSEEN + ")", 732 encodeFolderName(mName))); 733 // S: * STATUS mboxname (MESSAGES 231 UIDNEXT 44292) 734 for (ImapResponse response : responses) { 735 if (response.isDataResponse(0, ImapConstants.STATUS)) { 736 unreadMessageCount = response.getListOrEmpty(2) 737 .getKeyedStringOrEmpty(ImapConstants.UNSEEN).getNumberOrZero(); 738 } 739 } 740 return unreadMessageCount; 741 } catch (IOException ioe) { 742 throw ioExceptionHandler(mConnection, ioe); 743 } finally { 744 destroyResponses(); 745 } 746 } 747 748 @Override 749 public void delete(boolean recurse) throws MessagingException { 750 throw new Error("ImapStore.delete() not yet implemented"); 751 } 752 753 /* package */ String[] searchForUids(String searchCriteria) 754 throws MessagingException { 755 checkOpen(); 756 List<ImapResponse> responses; 757 try { 758 try { 759 responses = mConnection.executeSimpleCommand( 760 ImapConstants.UID_SEARCH + " " + searchCriteria); 761 } catch (ImapException e) { 762 return Utility.EMPTY_STRINGS; // not found; 763 } catch (IOException ioe) { 764 throw ioExceptionHandler(mConnection, ioe); 765 } 766 // S: * SEARCH 2 3 6 767 final ArrayList<String> uids = new ArrayList<String>(); 768 for (ImapResponse response : responses) { 769 if (!response.isDataResponse(0, ImapConstants.SEARCH)) { 770 continue; 771 } 772 // Found SEARCH response data 773 for (int i = 1; i < response.size(); i++) { 774 ImapString s = response.getStringOrEmpty(i); 775 if (s.isString()) { 776 uids.add(s.getString()); 777 } 778 } 779 } 780 return uids.toArray(Utility.EMPTY_STRINGS); 781 } finally { 782 destroyResponses(); 783 } 784 } 785 786 @Override 787 public Message getMessage(String uid) throws MessagingException { 788 checkOpen(); 789 790 String[] uids = searchForUids(ImapConstants.UID + " " + uid); 791 for (int i = 0; i < uids.length; i++) { 792 if (uids[i].equals(uid)) { 793 return new ImapMessage(uid, this); 794 } 795 } 796 return null; 797 } 798 799 @Override 800 public Message[] getMessages(int start, int end, MessageRetrievalListener listener) 801 throws MessagingException { 802 if (start < 1 || end < 1 || end < start) { 803 throw new MessagingException(String.format("Invalid range: %d %d", start, end)); 804 } 805 return getMessagesInternal( 806 searchForUids(String.format("%d:%d NOT DELETED", start, end)), listener); 807 } 808 809 @Override 810 public Message[] getMessages(MessageRetrievalListener listener) throws MessagingException { 811 return getMessages(null, listener); 812 } 813 814 @Override 815 public Message[] getMessages(String[] uids, MessageRetrievalListener listener) 816 throws MessagingException { 817 if (uids == null) { 818 uids = searchForUids("1:* NOT DELETED"); 819 } 820 return getMessagesInternal(uids, listener); 821 } 822 823 public Message[] getMessagesInternal(String[] uids, MessageRetrievalListener listener) 824 throws MessagingException { 825 final ArrayList<Message> messages = new ArrayList<Message>(uids.length); 826 for (int i = 0; i < uids.length; i++) { 827 final String uid = uids[i]; 828 final ImapMessage message = new ImapMessage(uid, this); 829 messages.add(message); 830 if (listener != null) { 831 listener.messageRetrieved(message); 832 } 833 } 834 return messages.toArray(Message.EMPTY_ARRAY); 835 } 836 837 @Override 838 public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener) 839 throws MessagingException { 840 try { 841 fetchInternal(messages, fp, listener); 842 } catch (RuntimeException e) { // Probably a parser error. 843 Log.w(Email.LOG_TAG, "Exception detected: " + e.getMessage()); 844 if (mConnection != null) { 845 mConnection.logLastDiscourse(); 846 } 847 throw e; 848 } 849 } 850 851 public void fetchInternal(Message[] messages, FetchProfile fp, 852 MessageRetrievalListener listener) throws MessagingException { 853 if (messages.length == 0) { 854 return; 855 } 856 checkOpen(); 857 HashMap<String, Message> messageMap = new HashMap<String, Message>(); 858 for (Message m : messages) { 859 messageMap.put(m.getUid(), m); 860 } 861 862 /* 863 * Figure out what command we are going to run: 864 * FLAGS - UID FETCH (FLAGS) 865 * ENVELOPE - UID FETCH (INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[ 866 * HEADER.FIELDS (date subject from content-type to cc)]) 867 * STRUCTURE - UID FETCH (BODYSTRUCTURE) 868 * BODY_SANE - UID FETCH (BODY.PEEK[]<0.N>) where N = max bytes returned 869 * BODY - UID FETCH (BODY.PEEK[]) 870 * Part - UID FETCH (BODY.PEEK[ID]) where ID = mime part ID 871 */ 872 873 final LinkedHashSet<String> fetchFields = new LinkedHashSet<String>(); 874 875 fetchFields.add(ImapConstants.UID); 876 if (fp.contains(FetchProfile.Item.FLAGS)) { 877 fetchFields.add(ImapConstants.FLAGS); 878 } 879 if (fp.contains(FetchProfile.Item.ENVELOPE)) { 880 fetchFields.add(ImapConstants.INTERNALDATE); 881 fetchFields.add(ImapConstants.RFC822_SIZE); 882 fetchFields.add(ImapConstants.FETCH_FIELD_HEADERS); 883 } 884 if (fp.contains(FetchProfile.Item.STRUCTURE)) { 885 fetchFields.add(ImapConstants.BODYSTRUCTURE); 886 } 887 888 if (fp.contains(FetchProfile.Item.BODY_SANE)) { 889 fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_SANE); 890 } 891 if (fp.contains(FetchProfile.Item.BODY)) { 892 fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK); 893 } 894 895 final Part fetchPart = fp.getFirstPart(); 896 if (fetchPart != null) { 897 String[] partIds = 898 fetchPart.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA); 899 if (partIds != null) { 900 fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_BARE 901 + "[" + partIds[0] + "]"); 902 } 903 } 904 905 try { 906 mConnection.sendCommand(String.format( 907 ImapConstants.UID_FETCH + " %s (%s)", joinMessageUids(messages), 908 Utility.combine(fetchFields.toArray(new String[fetchFields.size()]), ' ') 909 ), false); 910 ImapResponse response; 911 int messageNumber = 0; 912 do { 913 response = null; 914 try { 915 response = mConnection.readResponse(); 916 917 if (!response.isDataResponse(1, ImapConstants.FETCH)) { 918 continue; // Ignore 919 } 920 final ImapList fetchList = response.getListOrEmpty(2); 921 final String uid = fetchList.getKeyedStringOrEmpty(ImapConstants.UID) 922 .getString(); 923 if (TextUtils.isEmpty(uid)) continue; 924 925 ImapMessage message = (ImapMessage) messageMap.get(uid); 926 if (message == null) continue; 927 928 if (fp.contains(FetchProfile.Item.FLAGS)) { 929 final ImapList flags = 930 fetchList.getKeyedListOrEmpty(ImapConstants.FLAGS); 931 for (int i = 0, count = flags.size(); i < count; i++) { 932 final ImapString flag = flags.getStringOrEmpty(i); 933 if (flag.is(ImapConstants.FLAG_DELETED)) { 934 message.setFlagInternal(Flag.DELETED, true); 935 } else if (flag.is(ImapConstants.FLAG_ANSWERED)) { 936 message.setFlagInternal(Flag.ANSWERED, true); 937 } else if (flag.is(ImapConstants.FLAG_SEEN)) { 938 message.setFlagInternal(Flag.SEEN, true); 939 } else if (flag.is(ImapConstants.FLAG_FLAGGED)) { 940 message.setFlagInternal(Flag.FLAGGED, true); 941 } 942 } 943 } 944 if (fp.contains(FetchProfile.Item.ENVELOPE)) { 945 final Date internalDate = fetchList.getKeyedStringOrEmpty( 946 ImapConstants.INTERNALDATE).getDateOrNull(); 947 final int size = fetchList.getKeyedStringOrEmpty( 948 ImapConstants.RFC822_SIZE).getNumberOrZero(); 949 final String header = fetchList.getKeyedStringOrEmpty( 950 ImapConstants.BODY_BRACKET_HEADER, true).getString(); 951 952 message.setInternalDate(internalDate); 953 message.setSize(size); 954 message.parse(Utility.streamFromAsciiString(header)); 955 } 956 if (fp.contains(FetchProfile.Item.STRUCTURE)) { 957 ImapList bs = fetchList.getKeyedListOrEmpty( 958 ImapConstants.BODYSTRUCTURE); 959 if (!bs.isEmpty()) { 960 try { 961 parseBodyStructure(bs, message, ImapConstants.TEXT); 962 } catch (MessagingException e) { 963 if (Email.LOGD) { 964 Log.v(Email.LOG_TAG, "Error handling message", e); 965 } 966 message.setBody(null); 967 } 968 } 969 } 970 if (fp.contains(FetchProfile.Item.BODY) 971 || fp.contains(FetchProfile.Item.BODY_SANE)) { 972 // Body is keyed by "BODY[...". 973 // TOOD Should we accept "RFC822" as well?? 974 // The old code didn't really check the key, so it accepted any literal 975 // that first appeared. 976 ImapString body = fetchList.getKeyedStringOrEmpty("BODY[", true); 977 InputStream bodyStream = body.getAsStream(); 978 message.parse(bodyStream); 979 } 980 if (fetchPart != null && fetchPart.getSize() > 0) { 981 InputStream bodyStream = 982 fetchList.getKeyedStringOrEmpty("BODY[", true).getAsStream(); 983 String contentType = fetchPart.getContentType(); 984 String contentTransferEncoding = fetchPart.getHeader( 985 MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING)[0]; 986 987 // TODO Don't create 2 temp files. 988 // decodeBody creates BinaryTempFileBody, but we could avoid this 989 // if we implement ImapStringBody. 990 // (We'll need to share a temp file. Protect it with a ref-count.) 991 fetchPart.setBody(MimeUtility.decodeBody( 992 bodyStream, 993 contentTransferEncoding)); 994 } 995 996 if (listener != null) { 997 listener.messageRetrieved(message); 998 } 999 } finally { 1000 destroyResponses(); 1001 } 1002 } while (!response.isTagged()); 1003 } catch (IOException ioe) { 1004 throw ioExceptionHandler(mConnection, ioe); 1005 } 1006 } 1007 1008 @Override 1009 public Flag[] getPermanentFlags() throws MessagingException { 1010 return PERMANENT_FLAGS; 1011 } 1012 1013 /** 1014 * Handle any untagged responses that the caller doesn't care to handle themselves. 1015 * @param responses 1016 */ 1017 private void handleUntaggedResponses(List<ImapResponse> responses) { 1018 for (ImapResponse response : responses) { 1019 handleUntaggedResponse(response); 1020 } 1021 } 1022 1023 /** 1024 * Handle an untagged response that the caller doesn't care to handle themselves. 1025 * @param response 1026 */ 1027 private void handleUntaggedResponse(ImapResponse response) { 1028 if (response.isDataResponse(1, ImapConstants.EXISTS)) { 1029 mMessageCount = response.getStringOrEmpty(0).getNumberOrZero(); 1030 } 1031 } 1032 1033 private static void parseBodyStructure(ImapList bs, Part part, String id) 1034 throws MessagingException { 1035 if (bs.getElementOrNone(0).isList()) { 1036 /* 1037 * This is a multipart/* 1038 */ 1039 MimeMultipart mp = new MimeMultipart(); 1040 for (int i = 0, count = bs.size(); i < count; i++) { 1041 ImapElement e = bs.getElementOrNone(i); 1042 if (e.isList()) { 1043 /* 1044 * For each part in the message we're going to add a new BodyPart and parse 1045 * into it. 1046 */ 1047 MimeBodyPart bp = new MimeBodyPart(); 1048 if (id.equals(ImapConstants.TEXT)) { 1049 parseBodyStructure(bs.getListOrEmpty(i), bp, Integer.toString(i + 1)); 1050 1051 } else { 1052 parseBodyStructure(bs.getListOrEmpty(i), bp, id + "." + (i + 1)); 1053 } 1054 mp.addBodyPart(bp); 1055 1056 } else { 1057 if (e.isString()) { 1058 mp.setSubType(bs.getStringOrEmpty(i).getString().toLowerCase()); 1059 } 1060 break; // Ignore the rest of the list. 1061 } 1062 } 1063 part.setBody(mp); 1064 } else { 1065 /* 1066 * This is a body. We need to add as much information as we can find out about 1067 * it to the Part. 1068 */ 1069 1070 /* 1071 body type 1072 body subtype 1073 body parameter parenthesized list 1074 body id 1075 body description 1076 body encoding 1077 body size 1078 */ 1079 1080 final ImapString type = bs.getStringOrEmpty(0); 1081 final ImapString subType = bs.getStringOrEmpty(1); 1082 final String mimeType = 1083 (type.getString() + "/" + subType.getString()).toLowerCase(); 1084 1085 final ImapList bodyParams = bs.getListOrEmpty(2); 1086 final ImapString cid = bs.getStringOrEmpty(3); 1087 final ImapString encoding = bs.getStringOrEmpty(5); 1088 final int size = bs.getStringOrEmpty(6).getNumberOrZero(); 1089 1090 if (MimeUtility.mimeTypeMatches(mimeType, MimeUtility.MIME_TYPE_RFC822)) { 1091 // A body type of type MESSAGE and subtype RFC822 1092 // contains, immediately after the basic fields, the 1093 // envelope structure, body structure, and size in 1094 // text lines of the encapsulated message. 1095 // [MESSAGE, RFC822, [NAME, filename.eml], NIL, NIL, 7BIT, 5974, NIL, 1096 // [INLINE, [FILENAME*0, Fwd: Xxx..., FILENAME*1, filename.eml]], NIL] 1097 /* 1098 * This will be caught by fetch and handled appropriately. 1099 */ 1100 throw new MessagingException("BODYSTRUCTURE " + MimeUtility.MIME_TYPE_RFC822 1101 + " not yet supported."); 1102 } 1103 1104 /* 1105 * Set the content type with as much information as we know right now. 1106 */ 1107 final StringBuilder contentType = new StringBuilder(mimeType); 1108 1109 /* 1110 * If there are body params we might be able to get some more information out 1111 * of them. 1112 */ 1113 for (int i = 1, count = bodyParams.size(); i < count; i += 2) { 1114 1115 // TODO We need to convert " into %22, but 1116 // because MimeUtility.getHeaderParameter doesn't recognize it, 1117 // we can't fix it for now. 1118 contentType.append(String.format(";\n %s=\"%s\"", 1119 bodyParams.getStringOrEmpty(i - 1).getString(), 1120 bodyParams.getStringOrEmpty(i).getString())); 1121 } 1122 1123 part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType.toString()); 1124 1125 // Extension items 1126 final ImapList bodyDisposition; 1127 1128 if (type.is(ImapConstants.TEXT) && bs.getElementOrNone(9).isList()) { 1129 // If media-type is TEXT, 9th element might be: [body-fld-lines] := number 1130 // So, if it's not a list, use 10th element. 1131 // (Couldn't find evidence in the RFC if it's ALWAYS 10th element.) 1132 bodyDisposition = bs.getListOrEmpty(9); 1133 } else { 1134 bodyDisposition = bs.getListOrEmpty(8); 1135 } 1136 1137 final StringBuilder contentDisposition = new StringBuilder(); 1138 1139 if (bodyDisposition.size() > 0) { 1140 final String bodyDisposition0Str = 1141 bodyDisposition.getStringOrEmpty(0).getString().toLowerCase(); 1142 if (!TextUtils.isEmpty(bodyDisposition0Str)) { 1143 contentDisposition.append(bodyDisposition0Str); 1144 } 1145 1146 final ImapList bodyDispositionParams = bodyDisposition.getListOrEmpty(1); 1147 if (!bodyDispositionParams.isEmpty()) { 1148 /* 1149 * If there is body disposition information we can pull some more 1150 * information about the attachment out. 1151 */ 1152 for (int i = 1, count = bodyDispositionParams.size(); i < count; i += 2) { 1153 1154 // TODO We need to convert " into %22. See above. 1155 contentDisposition.append(String.format(";\n %s=\"%s\"", 1156 bodyDispositionParams.getStringOrEmpty(i - 1) 1157 .getString().toLowerCase(), 1158 bodyDispositionParams.getStringOrEmpty(i).getString())); 1159 } 1160 } 1161 } 1162 1163 if ((size > 0) 1164 && (MimeUtility.getHeaderParameter(contentDisposition.toString(), "size") 1165 == null)) { 1166 contentDisposition.append(String.format(";\n size=%d", size)); 1167 } 1168 1169 if (contentDisposition.length() > 0) { 1170 /* 1171 * Set the content disposition containing at least the size. Attachment 1172 * handling code will use this down the road. 1173 */ 1174 part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, 1175 contentDisposition.toString()); 1176 } 1177 1178 /* 1179 * Set the Content-Transfer-Encoding header. Attachment code will use this 1180 * to parse the body. 1181 */ 1182 if (!encoding.isEmpty()) { 1183 part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, 1184 encoding.getString()); 1185 } 1186 1187 /* 1188 * Set the Content-ID header. 1189 */ 1190 if (!cid.isEmpty()) { 1191 part.setHeader(MimeHeader.HEADER_CONTENT_ID, cid.getString()); 1192 } 1193 1194 if (size > 0) { 1195 if (part instanceof ImapMessage) { 1196 ((ImapMessage) part).setSize(size); 1197 } else if (part instanceof MimeBodyPart) { 1198 ((MimeBodyPart) part).setSize(size); 1199 } else { 1200 throw new MessagingException("Unknown part type " + part.toString()); 1201 } 1202 } 1203 part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id); 1204 } 1205 1206 } 1207 1208 /** 1209 * Appends the given messages to the selected folder. This implementation also determines 1210 * the new UID of the given message on the IMAP server and sets the Message's UID to the 1211 * new server UID. 1212 */ 1213 @Override 1214 public void appendMessages(Message[] messages) throws MessagingException { 1215 checkOpen(); 1216 try { 1217 for (Message message : messages) { 1218 // Create output count 1219 CountingOutputStream out = new CountingOutputStream(); 1220 EOLConvertingOutputStream eolOut = new EOLConvertingOutputStream(out); 1221 message.writeTo(eolOut); 1222 eolOut.flush(); 1223 // Create flag list (most often this will be "\SEEN") 1224 String flagList = ""; 1225 Flag[] flags = message.getFlags(); 1226 if (flags.length > 0) { 1227 StringBuilder sb = new StringBuilder(); 1228 for (int i = 0, count = flags.length; i < count; i++) { 1229 Flag flag = flags[i]; 1230 if (flag == Flag.SEEN) { 1231 sb.append(" " + ImapConstants.FLAG_SEEN); 1232 } else if (flag == Flag.FLAGGED) { 1233 sb.append(" " + ImapConstants.FLAG_FLAGGED); 1234 } 1235 } 1236 if (sb.length() > 0) { 1237 flagList = sb.substring(1); 1238 } 1239 } 1240 1241 mConnection.sendCommand( 1242 String.format(ImapConstants.APPEND + " \"%s\" (%s) {%d}", 1243 encodeFolderName(mName), 1244 flagList, 1245 out.getCount()), false); 1246 ImapResponse response; 1247 do { 1248 response = mConnection.readResponse(); 1249 if (response.isContinuationRequest()) { 1250 eolOut = new EOLConvertingOutputStream( 1251 mConnection.mTransport.getOutputStream()); 1252 message.writeTo(eolOut); 1253 eolOut.write('\r'); 1254 eolOut.write('\n'); 1255 eolOut.flush(); 1256 } else if (!response.isTagged()) { 1257 handleUntaggedResponse(response); 1258 } 1259 } while (!response.isTagged()); 1260 1261 // TODO Why not check the response? 1262 1263 /* 1264 * Try to recover the UID of the message from an APPENDUID response. 1265 * e.g. 11 OK [APPENDUID 2 238268] APPEND completed 1266 */ 1267 final ImapList appendList = response.getListOrEmpty(1); 1268 if ((appendList.size() >= 3) && appendList.is(0, ImapConstants.APPENDUID)) { 1269 String serverUid = appendList.getStringOrEmpty(2).getString(); 1270 if (!TextUtils.isEmpty(serverUid)) { 1271 message.setUid(serverUid); 1272 continue; 1273 } 1274 } 1275 1276 /* 1277 * Try to find the UID of the message we just appended using the 1278 * Message-ID header. If there are more than one response, take the 1279 * last one, as it's most likely the newest (the one we just uploaded). 1280 */ 1281 String messageId = message.getMessageId(); 1282 if (messageId == null || messageId.length() == 0) { 1283 continue; 1284 } 1285 String[] uids = searchForUids( 1286 String.format("(HEADER MESSAGE-ID %s)", messageId)); 1287 if (uids.length > 0) { 1288 message.setUid(uids[0]); 1289 } 1290 } 1291 } catch (IOException ioe) { 1292 throw ioExceptionHandler(mConnection, ioe); 1293 } finally { 1294 destroyResponses(); 1295 } 1296 } 1297 1298 @Override 1299 public Message[] expunge() throws MessagingException { 1300 checkOpen(); 1301 try { 1302 handleUntaggedResponses(mConnection.executeSimpleCommand(ImapConstants.EXPUNGE)); 1303 } catch (IOException ioe) { 1304 throw ioExceptionHandler(mConnection, ioe); 1305 } finally { 1306 destroyResponses(); 1307 } 1308 return null; 1309 } 1310 1311 @Override 1312 public void setFlags(Message[] messages, Flag[] flags, boolean value) 1313 throws MessagingException { 1314 checkOpen(); 1315 1316 String allFlags = ""; 1317 if (flags.length > 0) { 1318 StringBuilder flagList = new StringBuilder(); 1319 for (int i = 0, count = flags.length; i < count; i++) { 1320 Flag flag = flags[i]; 1321 if (flag == Flag.SEEN) { 1322 flagList.append(" " + ImapConstants.FLAG_SEEN); 1323 } else if (flag == Flag.DELETED) { 1324 flagList.append(" " + ImapConstants.FLAG_DELETED); 1325 } else if (flag == Flag.FLAGGED) { 1326 flagList.append(" " + ImapConstants.FLAG_FLAGGED); 1327 } 1328 } 1329 allFlags = flagList.substring(1); 1330 } 1331 try { 1332 mConnection.executeSimpleCommand(String.format( 1333 ImapConstants.UID_STORE + " %s %s" + ImapConstants.FLAGS_SILENT + " (%s)", 1334 joinMessageUids(messages), 1335 value ? "+" : "-", 1336 allFlags)); 1337 1338 } catch (IOException ioe) { 1339 throw ioExceptionHandler(mConnection, ioe); 1340 } finally { 1341 destroyResponses(); 1342 } 1343 } 1344 1345 private void checkOpen() throws MessagingException { 1346 if (!isOpen()) { 1347 throw new MessagingException("Folder " + mName + " is not open."); 1348 } 1349 } 1350 1351 private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) 1352 throws MessagingException { 1353 if (Email.DEBUG) { 1354 Log.d(Email.LOG_TAG, "IO Exception detected: ", ioe); 1355 } 1356 connection.destroyResponses(); 1357 connection.close(); 1358 if (connection == mConnection) { 1359 mConnection = null; // To prevent close() from returning the connection to the pool. 1360 close(false); 1361 } 1362 return new MessagingException("IO Error", ioe); 1363 } 1364 1365 @Override 1366 public boolean equals(Object o) { 1367 if (o instanceof ImapFolder) { 1368 return ((ImapFolder)o).mName.equals(mName); 1369 } 1370 return super.equals(o); 1371 } 1372 1373 @Override 1374 public Message createMessage(String uid) throws MessagingException { 1375 return new ImapMessage(uid, this); 1376 } 1377 } 1378 1379 /** 1380 * A cacheable class that stores the details for a single IMAP connection. 1381 */ 1382 class ImapConnection { 1383 private static final String IMAP_DEDACTED_LOG = "[IMAP command redacted]"; 1384 private Transport mTransport; 1385 private ImapResponseParser mParser; 1386 /** # of command/response lines to log upon crash. */ 1387 private static final int DISCOURSE_LOGGER_SIZE = 64; 1388 private final DiscourseLogger mDiscourse = new DiscourseLogger(DISCOURSE_LOGGER_SIZE); 1389 1390 public void open() throws IOException, MessagingException { 1391 if (mTransport != null && mTransport.isOpen()) { 1392 return; 1393 } 1394 1395 try { 1396 // copy configuration into a clean transport, if necessary 1397 if (mTransport == null) { 1398 mTransport = mRootTransport.newInstanceWithConfiguration(); 1399 } 1400 1401 mTransport.open(); 1402 mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT); 1403 1404 createParser(); 1405 1406 // BANNER 1407 mParser.readResponse(); 1408 1409 // CAPABILITY 1410 ImapResponse capabilityResponse = queryCapabilities(); 1411 1412 // TLS 1413 if (mTransport.canTryTlsSecurity()) { 1414 if (capabilityResponse.contains(ImapConstants.STARTTLS)) { 1415 // STARTTLS 1416 executeSimpleCommand(ImapConstants.STARTTLS); 1417 1418 mTransport.reopenTls(); 1419 mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT); 1420 createParser(); 1421 // Per RFC requirement (3501-6.2.1) gather new capabilities 1422 capabilityResponse = queryCapabilities(); 1423 } else { 1424 if (Config.LOGD && Email.DEBUG) { 1425 Log.d(Email.LOG_TAG, "TLS not supported but required"); 1426 } 1427 throw new MessagingException(MessagingException.TLS_REQUIRED); 1428 } 1429 } 1430 1431 // ID 1432 if (capabilityResponse.contains(ImapConstants.ID)) { 1433 // Assign user-agent string (for RFC2971 ID command) 1434 String mUserAgent = getImapId(mContext, mUsername, mRootTransport.getHost(), 1435 capabilityResponse); 1436 if (mUserAgent != null) { 1437 mIdPhrase = ImapConstants.ID + " (" + mUserAgent + ")"; 1438 } else if (DEBUG_FORCE_SEND_ID) { 1439 mIdPhrase = ImapConstants.ID + " " + ImapConstants.NIL; 1440 } 1441 // else: mIdPhrase = null, no ID will be emitted 1442 1443 // Send user-agent in an RFC2971 ID command 1444 if (mIdPhrase != null) { 1445 try { 1446 executeSimpleCommand(mIdPhrase); 1447 } catch (ImapException ie) { 1448 // Log for debugging, but this is not a fatal problem. 1449 if (Config.LOGD && Email.DEBUG) { 1450 Log.d(Email.LOG_TAG, ie.toString()); 1451 } 1452 } catch (IOException ioe) { 1453 // Special case to handle malformed OK responses and ignore them. 1454 // A true IOException will recur on the following login steps 1455 // This can go away after the parser is fixed - see bug 2138981 1456 } 1457 } 1458 } 1459 1460 // LOGIN 1461 try { 1462 // TODO eventually we need to add additional authentication 1463 // options such as SASL 1464 executeSimpleCommand(mLoginPhrase, true); 1465 } catch (ImapException ie) { 1466 if (Config.LOGD && Email.DEBUG) { 1467 Log.d(Email.LOG_TAG, ie.toString()); 1468 } 1469 throw new AuthenticationFailedException(ie.getAlertText(), ie); 1470 1471 } catch (MessagingException me) { 1472 throw new AuthenticationFailedException(null, me); 1473 } 1474 } catch (SSLException e) { 1475 if (Config.LOGD && Email.DEBUG) { 1476 Log.d(Email.LOG_TAG, e.toString()); 1477 } 1478 throw new CertificateValidationException(e.getMessage(), e); 1479 } catch (IOException ioe) { 1480 // NOTE: Unlike similar code in POP3, I'm going to rethrow as-is. There is a lot 1481 // of other code here that catches IOException and I don't want to break it. 1482 // This catch is only here to enhance logging of connection-time issues. 1483 if (Config.LOGD && Email.DEBUG) { 1484 Log.d(Email.LOG_TAG, ioe.toString()); 1485 } 1486 throw ioe; 1487 } finally { 1488 destroyResponses(); 1489 } 1490 } 1491 1492 public void close() { 1493 if (mTransport != null) { 1494 mTransport.close(); 1495 mTransport = null; 1496 } 1497 } 1498 1499 /** 1500 * Create an {@link ImapResponseParser} from {@code mTransport.getInputStream()} and 1501 * set it to {@link #mParser}. 1502 * 1503 * If we already have an {@link ImapResponseParser}, we 1504 * {@link #destroyResponses()} and throw it away. 1505 */ 1506 private void createParser() { 1507 destroyResponses(); 1508 mParser = new ImapResponseParser(mTransport.getInputStream(), mDiscourse); 1509 } 1510 1511 public void destroyResponses() { 1512 if (mParser != null) { 1513 mParser.destroyResponses(); 1514 } 1515 } 1516 1517 /* package */ boolean isTransportOpenForTest() { 1518 return mTransport != null ? mTransport.isOpen() : false; 1519 } 1520 1521 public ImapResponse readResponse() throws IOException, MessagingException { 1522 return mParser.readResponse(); 1523 } 1524 1525 /** 1526 * Send a single command to the server. The command will be preceded by an IMAP command 1527 * tag and followed by \r\n (caller need not supply them). 1528 * 1529 * @param command The command to send to the server 1530 * @param sensitive If true, the command will not be logged 1531 * @return Returns the command tag that was sent 1532 */ 1533 public String sendCommand(String command, boolean sensitive) 1534 throws MessagingException, IOException { 1535 open(); 1536 String tag = Integer.toString(mNextCommandTag.incrementAndGet()); 1537 String commandToSend = tag + " " + command; 1538 mTransport.writeLine(commandToSend, sensitive ? IMAP_DEDACTED_LOG : null); 1539 mDiscourse.addSentCommand(sensitive ? IMAP_DEDACTED_LOG : commandToSend); 1540 return tag; 1541 } 1542 1543 public List<ImapResponse> executeSimpleCommand(String command) throws IOException, 1544 MessagingException { 1545 return executeSimpleCommand(command, false); 1546 } 1547 1548 public List<ImapResponse> executeSimpleCommand(String command, boolean sensitive) 1549 throws IOException, MessagingException { 1550 String tag = sendCommand(command, sensitive); 1551 ArrayList<ImapResponse> responses = new ArrayList<ImapResponse>(); 1552 ImapResponse response; 1553 do { 1554 response = mParser.readResponse(); 1555 responses.add(response); 1556 } while (!response.isTagged()); 1557 if (!response.isOk()) { 1558 final String toString = response.toString(); 1559 final String alert = response.getAlertTextOrEmpty().getString(); 1560 destroyResponses(); 1561 throw new ImapException(toString, alert); 1562 } 1563 return responses; 1564 } 1565 1566 /** 1567 * Query server for capabilities. 1568 */ 1569 private ImapResponse queryCapabilities() throws IOException, MessagingException { 1570 ImapResponse capabilityResponse = null; 1571 for (ImapResponse r : executeSimpleCommand(ImapConstants.CAPABILITY)) { 1572 if (r.is(0, ImapConstants.CAPABILITY)) { 1573 capabilityResponse = r; 1574 break; 1575 } 1576 } 1577 if (capabilityResponse == null) { 1578 throw new MessagingException("Invalid CAPABILITY response received"); 1579 } 1580 return capabilityResponse; 1581 } 1582 1583 /** @see ImapResponseParser#logLastDiscourse() */ 1584 public void logLastDiscourse() { 1585 mDiscourse.logLastDiscourse(); 1586 } 1587 } 1588 1589 static class ImapMessage extends MimeMessage { 1590 ImapMessage(String uid, Folder folder) throws MessagingException { 1591 this.mUid = uid; 1592 this.mFolder = folder; 1593 } 1594 1595 public void setSize(int size) { 1596 this.mSize = size; 1597 } 1598 1599 @Override 1600 public void parse(InputStream in) throws IOException, MessagingException { 1601 super.parse(in); 1602 } 1603 1604 public void setFlagInternal(Flag flag, boolean set) throws MessagingException { 1605 super.setFlag(flag, set); 1606 } 1607 1608 @Override 1609 public void setFlag(Flag flag, boolean set) throws MessagingException { 1610 super.setFlag(flag, set); 1611 mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set); 1612 } 1613 } 1614 1615 static class ImapException extends MessagingException { 1616 String mAlertText; 1617 1618 public ImapException(String message, String alertText, Throwable throwable) { 1619 super(message, throwable); 1620 this.mAlertText = alertText; 1621 } 1622 1623 public ImapException(String message, String alertText) { 1624 super(message); 1625 this.mAlertText = alertText; 1626 } 1627 1628 public String getAlertText() { 1629 return mAlertText; 1630 } 1631 1632 public void setAlertText(String alertText) { 1633 mAlertText = alertText; 1634 } 1635 } 1636} 1637