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