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