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