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