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