ImapStore.java revision 56e48981f0093c737b38b7757410283be9ed823f
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 if (part.getSize() > 0) { 759 InputStream bodyStream = 760 fetchList.getLiteral(fetchList.size() - 1); 761 String contentType = part.getContentType(); 762 String contentTransferEncoding = part.getHeader( 763 MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING)[0]; 764 part.setBody(MimeUtility.decodeBody( 765 bodyStream, 766 contentTransferEncoding)); 767 } 768 } 769 } 770 771 if (listener != null) { 772 listener.messageFinished(message, messageNumber, messageMap.size()); 773 } 774 } 775 776 while (response.more()); 777 778 } while (response.mTag == null); 779 } 780 catch (IOException ioe) { 781 throw ioExceptionHandler(mConnection, ioe); 782 } 783 } 784 785 @Override 786 public Flag[] getPermanentFlags() throws MessagingException { 787 return PERMANENT_FLAGS; 788 } 789 790 /** 791 * Handle any untagged responses that the caller doesn't care to handle themselves. 792 * @param responses 793 */ 794 private void handleUntaggedResponses(List<ImapResponse> responses) { 795 for (ImapResponse response : responses) { 796 handleUntaggedResponse(response); 797 } 798 } 799 800 /** 801 * Handle an untagged response that the caller doesn't care to handle themselves. 802 * @param response 803 */ 804 private void handleUntaggedResponse(ImapResponse response) { 805 if (response.mTag == null && response.get(1).equals("EXISTS")) { 806 mMessageCount = response.getNumber(0); 807 } 808 } 809 810 private void parseBodyStructure(ImapList bs, Part part, String id) 811 throws MessagingException { 812 if (bs.get(0) instanceof ImapList) { 813 /* 814 * This is a multipart/* 815 */ 816 MimeMultipart mp = new MimeMultipart(); 817 for (int i = 0, count = bs.size(); i < count; i++) { 818 if (bs.get(i) instanceof ImapList) { 819 /* 820 * For each part in the message we're going to add a new BodyPart and parse 821 * into it. 822 */ 823 ImapBodyPart bp = new ImapBodyPart(); 824 if (id.equals("TEXT")) { 825 parseBodyStructure(bs.getList(i), bp, Integer.toString(i + 1)); 826 } 827 else { 828 parseBodyStructure(bs.getList(i), bp, id + "." + (i + 1)); 829 } 830 mp.addBodyPart(bp); 831 } 832 else { 833 /* 834 * We've got to the end of the children of the part, so now we can find out 835 * what type it is and bail out. 836 */ 837 String subType = bs.getString(i); 838 mp.setSubType(subType.toLowerCase()); 839 break; 840 } 841 } 842 part.setBody(mp); 843 } 844 else{ 845 /* 846 * This is a body. We need to add as much information as we can find out about 847 * it to the Part. 848 */ 849 850 /* 851 body type 852 body subtype 853 body parameter parenthesized list 854 body id 855 body description 856 body encoding 857 body size 858 */ 859 860 861 String type = bs.getString(0); 862 String subType = bs.getString(1); 863 String mimeType = (type + "/" + subType).toLowerCase(); 864 865 ImapList bodyParams = null; 866 if (bs.get(2) instanceof ImapList) { 867 bodyParams = bs.getList(2); 868 } 869 String cid = bs.getString(3); 870 String encoding = bs.getString(5); 871 int size = bs.getNumber(6); 872 873 if (MimeUtility.mimeTypeMatches(mimeType, "message/rfc822")) { 874// A body type of type MESSAGE and subtype RFC822 875// contains, immediately after the basic fields, the 876// envelope structure, body structure, and size in 877// text lines of the encapsulated message. 878// [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] 879 /* 880 * This will be caught by fetch and handled appropriately. 881 */ 882 throw new MessagingException("BODYSTRUCTURE message/rfc822 not yet supported."); 883 } 884 885 /* 886 * Set the content type with as much information as we know right now. 887 */ 888 String contentType = String.format("%s", mimeType); 889 890 if (bodyParams != null) { 891 /* 892 * If there are body params we might be able to get some more information out 893 * of them. 894 */ 895 for (int i = 0, count = bodyParams.size(); i < count; i += 2) { 896 contentType += String.format(";\n %s=\"%s\"", 897 bodyParams.getString(i), 898 bodyParams.getString(i + 1)); 899 } 900 } 901 902 part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType); 903 904 // Extension items 905 ImapList bodyDisposition = null; 906 if (("text".equalsIgnoreCase(type)) 907 && (bs.size() > 8) 908 && (bs.get(9) instanceof ImapList)) { 909 bodyDisposition = bs.getList(9); 910 } 911 else if (!("text".equalsIgnoreCase(type)) 912 && (bs.size() > 7) 913 && (bs.get(8) instanceof ImapList)) { 914 bodyDisposition = bs.getList(8); 915 } 916 917 String contentDisposition = ""; 918 919 if (bodyDisposition != null && bodyDisposition.size() > 0) { 920 if (!"NIL".equalsIgnoreCase(bodyDisposition.getString(0))) { 921 contentDisposition = bodyDisposition.getString(0).toLowerCase(); 922 } 923 924 if ((bodyDisposition.size() > 1) 925 && (bodyDisposition.get(1) instanceof ImapList)) { 926 ImapList bodyDispositionParams = bodyDisposition.getList(1); 927 /* 928 * If there is body disposition information we can pull some more information 929 * about the attachment out. 930 */ 931 for (int i = 0, count = bodyDispositionParams.size(); i < count; i += 2) { 932 contentDisposition += String.format(";\n %s=\"%s\"", 933 bodyDispositionParams.getString(i).toLowerCase(), 934 bodyDispositionParams.getString(i + 1)); 935 } 936 } 937 } 938 939 if (MimeUtility.getHeaderParameter(contentDisposition, "size") == null) { 940 contentDisposition += String.format(";\n size=%d", size); 941 } 942 943 /* 944 * Set the content disposition containing at least the size. Attachment 945 * handling code will use this down the road. 946 */ 947 part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, contentDisposition); 948 949 950 /* 951 * Set the Content-Transfer-Encoding header. Attachment code will use this 952 * to parse the body. 953 */ 954 part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, encoding); 955 /* 956 * Set the Content-ID header. 957 */ 958 if (!"NIL".equalsIgnoreCase(cid)) { 959 part.setHeader(MimeHeader.HEADER_CONTENT_ID, cid); 960 } 961 962 if (part instanceof ImapMessage) { 963 ((ImapMessage) part).setSize(size); 964 } 965 else if (part instanceof ImapBodyPart) { 966 ((ImapBodyPart) part).setSize(size); 967 } 968 else { 969 throw new MessagingException("Unknown part type " + part.toString()); 970 } 971 part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id); 972 } 973 974 } 975 976 /** 977 * Appends the given messages to the selected folder. This implementation also determines 978 * the new UID of the given message on the IMAP server and sets the Message's UID to the 979 * new server UID. 980 */ 981 public void appendMessages(Message[] messages) throws MessagingException { 982 checkOpen(); 983 try { 984 for (Message message : messages) { 985 // Create output count 986 CountingOutputStream out = new CountingOutputStream(); 987 EOLConvertingOutputStream eolOut = new EOLConvertingOutputStream(out); 988 message.writeTo(eolOut); 989 eolOut.flush(); 990 // Create flag list (most often this will be "\SEEN") 991 String flagList = ""; 992 Flag[] flags = message.getFlags(); 993 if (flags.length > 0) { 994 StringBuilder sb = new StringBuilder(); 995 for (int i = 0, count = flags.length; i < count; i++) { 996 Flag flag = flags[i]; 997 if (flag == Flag.SEEN) { 998 sb.append(" \\Seen"); 999 } else if (flag == Flag.FLAGGED) { 1000 sb.append(" \\Flagged"); 1001 } 1002 } 1003 if (sb.length() > 0) { 1004 flagList = sb.substring(1); 1005 } 1006 } 1007 1008 mConnection.sendCommand( 1009 String.format("APPEND \"%s\" (%s) {%d}", 1010 encodeFolderName(mName), 1011 flagList, 1012 out.getCount()), false); 1013 ImapResponse response; 1014 do { 1015 response = mConnection.readResponse(); 1016 if (response.mCommandContinuationRequested) { 1017 eolOut = new EOLConvertingOutputStream(mConnection.mTransport.getOutputStream()); 1018 message.writeTo(eolOut); 1019 eolOut.write('\r'); 1020 eolOut.write('\n'); 1021 eolOut.flush(); 1022 } 1023 else if (response.mTag == null) { 1024 handleUntaggedResponse(response); 1025 } 1026 while (response.more()); 1027 } while(response.mTag == null); 1028 1029 /* 1030 * Try to find the UID of the message we just appended using the 1031 * Message-ID header. If there are more than one response, take the 1032 * last one, as it's most likely he newest (the one we just uploaded). 1033 */ 1034 String[] messageIdHeader = message.getHeader("Message-ID"); 1035 if (messageIdHeader == null || messageIdHeader.length == 0) { 1036 continue; 1037 } 1038 String messageId = messageIdHeader[0]; 1039 List<ImapResponse> responses = 1040 mConnection.executeSimpleCommand( 1041 String.format("UID SEARCH (HEADER MESSAGE-ID %s)", messageId)); 1042 for (ImapResponse response1 : responses) { 1043 if (response1.mTag == null && response1.get(0).equals("SEARCH") 1044 && response1.size() > 1) { 1045 message.setUid(response1.getString(response1.size()-1)); 1046 } 1047 } 1048 1049 } 1050 } 1051 catch (IOException ioe) { 1052 throw ioExceptionHandler(mConnection, ioe); 1053 } 1054 } 1055 1056 public Message[] expunge() throws MessagingException { 1057 checkOpen(); 1058 try { 1059 handleUntaggedResponses(mConnection.executeSimpleCommand("EXPUNGE")); 1060 } catch (IOException ioe) { 1061 throw ioExceptionHandler(mConnection, ioe); 1062 } 1063 return null; 1064 } 1065 1066 public void setFlags(Message[] messages, Flag[] flags, boolean value) 1067 throws MessagingException { 1068 checkOpen(); 1069 StringBuilder uidList = new StringBuilder(); 1070 for (int i = 0, count = messages.length; i < count; i++) { 1071 if (i > 0) uidList.append(','); 1072 uidList.append(messages[i].getUid()); 1073 } 1074 1075 StringBuilder flagList = new StringBuilder(); 1076 for (int i = 0, count = flags.length; i < count; i++) { 1077 Flag flag = flags[i]; 1078 if (flag == Flag.SEEN) { 1079 flagList.append(" \\Seen"); 1080 } else if (flag == Flag.DELETED) { 1081 flagList.append(" \\Deleted"); 1082 } else if (flag == Flag.FLAGGED) { 1083 flagList.append(" \\Flagged"); 1084 } 1085 } 1086 try { 1087 mConnection.executeSimpleCommand(String.format("UID STORE %s %sFLAGS.SILENT (%s)", 1088 uidList, 1089 value ? "+" : "-", 1090 flagList.substring(1))); // Remove the first space 1091 } 1092 catch (IOException ioe) { 1093 throw ioExceptionHandler(mConnection, ioe); 1094 } 1095 } 1096 1097 private void checkOpen() throws MessagingException { 1098 if (!isOpen()) { 1099 throw new MessagingException("Folder " + mName + " is not open."); 1100 } 1101 } 1102 1103 private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) 1104 throws MessagingException { 1105 connection.close(); 1106 close(false); 1107 return new MessagingException("IO Error", ioe); 1108 } 1109 1110 @Override 1111 public boolean equals(Object o) { 1112 if (o instanceof ImapFolder) { 1113 return ((ImapFolder)o).mName.equals(mName); 1114 } 1115 return super.equals(o); 1116 } 1117 1118 @Override 1119 public Message createMessage(String uid) throws MessagingException { 1120 return new ImapMessage(uid, this); 1121 } 1122 } 1123 1124 /** 1125 * A cacheable class that stores the details for a single IMAP connection. 1126 */ 1127 class ImapConnection { 1128 private Transport mTransport; 1129 private ImapResponseParser mParser; 1130 private int mNextCommandTag; 1131 1132 public void open() throws IOException, MessagingException { 1133 if (mTransport != null && mTransport.isOpen()) { 1134 return; 1135 } 1136 1137 mNextCommandTag = 1; 1138 1139 try { 1140 // copy configuration into a clean transport, if necessary 1141 if (mTransport == null) { 1142 mTransport = mRootTransport.newInstanceWithConfiguration(); 1143 } 1144 1145 mTransport.open(); 1146 mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT); 1147 1148 mParser = new ImapResponseParser(mTransport.getInputStream()); 1149 1150 // BANNER 1151 mParser.readResponse(); 1152 1153 if (mTransport.canTryTlsSecurity()) { 1154 // CAPABILITY 1155 List<ImapResponse> responses = executeSimpleCommand("CAPABILITY"); 1156 if (responses.size() != 2) { 1157 throw new MessagingException("Invalid CAPABILITY response received"); 1158 } 1159 if (responses.get(0).contains("STARTTLS")) { 1160 // STARTTLS 1161 executeSimpleCommand("STARTTLS"); 1162 1163 mTransport.reopenTls(); 1164 mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT); 1165 mParser = new ImapResponseParser(mTransport.getInputStream()); 1166 } else { 1167 if (Config.LOGD && Email.DEBUG) { 1168 Log.d(Email.LOG_TAG, "TLS not supported but required"); 1169 } 1170 throw new MessagingException(MessagingException.TLS_REQUIRED); 1171 } 1172 } 1173 1174 try { 1175 // TODO eventually we need to add additional authentication 1176 // options such as SASL 1177 executeSimpleCommand(mLoginPhrase, true); 1178 } catch (ImapException ie) { 1179 if (Config.LOGD && Email.DEBUG) { 1180 Log.d(Email.LOG_TAG, ie.toString()); 1181 } 1182 throw new AuthenticationFailedException(ie.getAlertText(), ie); 1183 1184 } catch (MessagingException me) { 1185 throw new AuthenticationFailedException(null, me); 1186 } 1187 } catch (SSLException e) { 1188 if (Config.LOGD && Email.DEBUG) { 1189 Log.d(Email.LOG_TAG, e.toString()); 1190 } 1191 throw new CertificateValidationException(e.getMessage(), e); 1192 } catch (IOException ioe) { 1193 // NOTE: Unlike similar code in POP3, I'm going to rethrow as-is. There is a lot 1194 // of other code here that catches IOException and I don't want to break it. 1195 // This catch is only here to enhance logging of connection-time issues. 1196 if (Config.LOGD && Email.DEBUG) { 1197 Log.d(Email.LOG_TAG, ioe.toString()); 1198 } 1199 throw ioe; 1200 } 1201 } 1202 1203 public void close() { 1204// if (isOpen()) { 1205// try { 1206// executeSimpleCommand("LOGOUT"); 1207// } catch (Exception e) { 1208// 1209// } 1210// } 1211 if (mTransport != null) { 1212 mTransport.close(); 1213 } 1214 } 1215 1216 public ImapResponse readResponse() throws IOException, MessagingException { 1217 return mParser.readResponse(); 1218 } 1219 1220 /** 1221 * Send a single command to the server. The command will be preceded by an IMAP command 1222 * tag and followed by \r\n (caller need not supply them). 1223 * 1224 * @param command The command to send to the server 1225 * @param sensitive If true, the command will not be logged 1226 * @return Returns the command tag that was sent 1227 */ 1228 public String sendCommand(String command, boolean sensitive) 1229 throws MessagingException, IOException { 1230 open(); 1231 String tag = Integer.toString(mNextCommandTag++); 1232 String commandToSend = tag + " " + command; 1233 mTransport.writeLine(commandToSend, sensitive ? "[IMAP command redacted]" : null); 1234 return tag; 1235 } 1236 1237 public List<ImapResponse> executeSimpleCommand(String command) throws IOException, 1238 ImapException, MessagingException { 1239 return executeSimpleCommand(command, false); 1240 } 1241 1242 public List<ImapResponse> executeSimpleCommand(String command, boolean sensitive) 1243 throws IOException, ImapException, MessagingException { 1244 String tag = sendCommand(command, sensitive); 1245 ArrayList<ImapResponse> responses = new ArrayList<ImapResponse>(); 1246 ImapResponse response; 1247 ImapResponse previous = null; 1248 do { 1249 // This is work around to parse literal in the middle of response. 1250 // We should nail down the previous response literal string if any. 1251 if (previous != null && !previous.completed()) { 1252 previous.nailDown(); 1253 } 1254 response = mParser.readResponse(); 1255 // This is work around to parse literal in the middle of response. 1256 // If we found unmatched tagged response, it possibly be the continuous 1257 // response just after the literal string. 1258 if (response.mTag != null && !response.mTag.equals(tag) 1259 && previous != null && !previous.completed()) { 1260 previous.appendAll(response); 1261 response.mTag = null; 1262 continue; 1263 } 1264 responses.add(response); 1265 previous = response; 1266 } while (response.mTag == null); 1267 if (response.size() < 1 || !response.get(0).equals("OK")) { 1268 throw new ImapException(response.toString(), response.getAlertText()); 1269 } 1270 return responses; 1271 } 1272 } 1273 1274 class ImapMessage extends MimeMessage { 1275 ImapMessage(String uid, Folder folder) throws MessagingException { 1276 this.mUid = uid; 1277 this.mFolder = folder; 1278 } 1279 1280 public void setSize(int size) { 1281 this.mSize = size; 1282 } 1283 1284 public void parse(InputStream in) throws IOException, MessagingException { 1285 super.parse(in); 1286 } 1287 1288 public void setFlagInternal(Flag flag, boolean set) throws MessagingException { 1289 super.setFlag(flag, set); 1290 } 1291 1292 @Override 1293 public void setFlag(Flag flag, boolean set) throws MessagingException { 1294 super.setFlag(flag, set); 1295 mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set); 1296 } 1297 } 1298 1299 class ImapBodyPart extends MimeBodyPart { 1300 public ImapBodyPart() throws MessagingException { 1301 super(); 1302 } 1303 1304// public void setSize(int size) { 1305// this.mSize = size; 1306// } 1307 } 1308 1309 class ImapException extends MessagingException { 1310 String mAlertText; 1311 1312 public ImapException(String message, String alertText, Throwable throwable) { 1313 super(message, throwable); 1314 this.mAlertText = alertText; 1315 } 1316 1317 public ImapException(String message, String alertText) { 1318 super(message); 1319 this.mAlertText = alertText; 1320 } 1321 1322 public String getAlertText() { 1323 return mAlertText; 1324 } 1325 1326 public void setAlertText(String alertText) { 1327 mAlertText = alertText; 1328 } 1329 } 1330} 1331