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