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