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