ImapStore.java revision 0d1078363581db8caded06cf94e729e88a88761a
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 public static final int CONNECTION_SECURITY_NONE = 0; 82 public static final int CONNECTION_SECURITY_TLS_OPTIONAL = 1; 83 public static final int CONNECTION_SECURITY_TLS_REQUIRED = 2; 84 public static final int CONNECTION_SECURITY_SSL_REQUIRED = 3; 85 public static final int CONNECTION_SECURITY_SSL_OPTIONAL = 4; 86 87 private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED, Flag.SEEN }; 88 89 private Transport mRootTransport; 90 private String mUsername; 91 private String mPassword; 92 private String mLoginPhrase; 93 private String mPathPrefix; 94 95 private LinkedList<ImapConnection> mConnections = 96 new LinkedList<ImapConnection>(); 97 98 /** 99 * Charset used for converting folder names to and from UTF-7 as defined by RFC 3501. 100 */ 101 private Charset mModifiedUtf7Charset; 102 103 /** 104 * Cache of ImapFolder objects. ImapFolders are attached to a given folder on the server 105 * and as long as their associated connection remains open they are reusable between 106 * requests. This cache lets us make sure we always reuse, if possible, for a given 107 * folder name. 108 */ 109 private HashMap<String, ImapFolder> mFolderCache = new HashMap<String, ImapFolder>(); 110 111 /** 112 * Static named constructor. 113 */ 114 public static Store newInstance(String uri, Context context, PersistentDataCallbacks callbacks) 115 throws MessagingException { 116 return new ImapStore(uri); 117 } 118 119 /** 120 * Allowed formats for the Uri: 121 * imap://user:password@server:port CONNECTION_SECURITY_NONE 122 * imap+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL 123 * imap+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED 124 * imap+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED 125 * imap+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL 126 * 127 * @param uriString the Uri containing information to configure this store 128 */ 129 private ImapStore(String uriString) throws MessagingException { 130 URI uri; 131 try { 132 uri = new URI(uriString); 133 } catch (URISyntaxException use) { 134 throw new MessagingException("Invalid ImapStore URI", use); 135 } 136 137 String scheme = uri.getScheme(); 138 int connectionSecurity = Transport.CONNECTION_SECURITY_NONE; 139 int defaultPort = -1; 140 if (scheme.equals("imap")) { 141 connectionSecurity = CONNECTION_SECURITY_NONE; 142 defaultPort = 143; 143 } else if (scheme.equals("imap+tls")) { 144 connectionSecurity = CONNECTION_SECURITY_TLS_OPTIONAL; 145 defaultPort = 143; 146 } else if (scheme.equals("imap+tls+")) { 147 connectionSecurity = CONNECTION_SECURITY_TLS_REQUIRED; 148 defaultPort = 143; 149 } else if (scheme.equals("imap+ssl+")) { 150 connectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED; 151 defaultPort = 993; 152 } else if (scheme.equals("imap+ssl")) { 153 connectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL; 154 defaultPort = 993; 155 } else { 156 throw new MessagingException("Unsupported protocol"); 157 } 158 159 mRootTransport = new MailTransport("IMAP"); 160 mRootTransport.setUri(uri, defaultPort); 161 mRootTransport.setSecurity(connectionSecurity); 162 163 String[] userInfoParts = mRootTransport.getUserInfoParts(); 164 if (userInfoParts != null) { 165 mUsername = userInfoParts[0]; 166 if (userInfoParts.length > 1) { 167 mPassword = userInfoParts[1]; 168 169 // build the LOGIN string once (instead of over-and-over again.) 170 // apply the quoting here around the built-up password 171 mLoginPhrase = "LOGIN " + mUsername + " " + Utility.imapQuoted(mPassword); 172 } 173 } 174 175 if ((uri.getPath() != null) && (uri.getPath().length() > 0)) { 176 mPathPrefix = uri.getPath().substring(1); 177 } 178 179 mModifiedUtf7Charset = new CharsetProvider().charsetForName("X-RFC-3501"); 180 } 181 182 /** 183 * For testing only. Injects a different root transport (it will be copied using 184 * newInstanceWithConfiguration() each time IMAP sets up a new channel). The transport 185 * should already be set up and ready to use. Do not use for real code. 186 * @param testTransport The Transport to inject and use for all future communication. 187 */ 188 /* package */ void setTransport(Transport testTransport) { 189 mRootTransport = testTransport; 190 } 191 192 @Override 193 public Folder getFolder(String name) throws MessagingException { 194 ImapFolder folder; 195 synchronized (mFolderCache) { 196 folder = mFolderCache.get(name); 197 if (folder == null) { 198 folder = new ImapFolder(name); 199 mFolderCache.put(name, folder); 200 } 201 } 202 return folder; 203 } 204 205 206 @Override 207 public Folder[] getPersonalNamespaces() throws MessagingException { 208 ImapConnection connection = getConnection(); 209 try { 210 ArrayList<Folder> folders = new ArrayList<Folder>(); 211 List<ImapResponse> responses = 212 connection.executeSimpleCommand(String.format("LIST \"\" \"%s*\"", 213 mPathPrefix == null ? "" : mPathPrefix)); 214 for (ImapResponse response : responses) { 215 if (response.get(0).equals("LIST")) { 216 boolean includeFolder = true; 217 String folder = decodeFolderName(response.getString(3)); 218 if (folder.equalsIgnoreCase("INBOX")) { 219 continue; 220 } 221 ImapList attributes = response.getList(1); 222 for (int i = 0, count = attributes.size(); i < count; i++) { 223 String attribute = attributes.getString(i); 224 if (attribute.equalsIgnoreCase("\\NoSelect")) { 225 includeFolder = false; 226 } 227 } 228 if (includeFolder) { 229 folders.add(getFolder(folder)); 230 } 231 } 232 } 233 folders.add(getFolder("INBOX")); 234 return folders.toArray(new Folder[] {}); 235 } catch (IOException ioe) { 236 connection.close(); 237 throw new MessagingException("Unable to get folder list.", ioe); 238 } finally { 239 releaseConnection(connection); 240 } 241 } 242 243 @Override 244 public void checkSettings() throws MessagingException { 245 try { 246 ImapConnection connection = new ImapConnection(); 247 connection.open(); 248 connection.close(); 249 } 250 catch (IOException ioe) { 251 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 252 } 253 } 254 255 /** 256 * Gets a connection if one is available for reuse, or creates a new one if not. 257 * @return 258 */ 259 private ImapConnection getConnection() throws MessagingException { 260 synchronized (mConnections) { 261 ImapConnection connection = null; 262 while ((connection = mConnections.poll()) != null) { 263 try { 264 connection.executeSimpleCommand("NOOP"); 265 break; 266 } 267 catch (IOException ioe) { 268 connection.close(); 269 } 270 } 271 if (connection == null) { 272 connection = new ImapConnection(); 273 } 274 return connection; 275 } 276 } 277 278 private void releaseConnection(ImapConnection connection) { 279 mConnections.offer(connection); 280 } 281 282 private String encodeFolderName(String name) { 283 try { 284 ByteBuffer bb = mModifiedUtf7Charset.encode(name); 285 byte[] b = new byte[bb.limit()]; 286 bb.get(b); 287 return new String(b, "US-ASCII"); 288 } 289 catch (UnsupportedEncodingException uee) { 290 /* 291 * The only thing that can throw this is getBytes("US-ASCII") and if US-ASCII doesn't 292 * exist we're totally screwed. 293 */ 294 throw new RuntimeException("Unabel to encode folder name: " + name, uee); 295 } 296 } 297 298 private String decodeFolderName(String name) { 299 /* 300 * Convert the encoded name to US-ASCII, then pass it through the modified UTF-7 301 * decoder and return the Unicode String. 302 */ 303 try { 304 byte[] encoded = name.getBytes("US-ASCII"); 305 CharBuffer cb = mModifiedUtf7Charset.decode(ByteBuffer.wrap(encoded)); 306 return cb.toString(); 307 } 308 catch (UnsupportedEncodingException uee) { 309 /* 310 * The only thing that can throw this is getBytes("US-ASCII") and if US-ASCII doesn't 311 * exist we're totally screwed. 312 */ 313 throw new RuntimeException("Unable to decode folder name: " + name, uee); 314 } 315 } 316 317 class ImapFolder extends Folder { 318 private String mName; 319 private int mMessageCount = -1; 320 private ImapConnection mConnection; 321 private OpenMode mMode; 322 private boolean mExists; 323 324 public ImapFolder(String name) { 325 this.mName = name; 326 } 327 328 public void open(OpenMode mode, PersistentDataCallbacks callbacks) 329 throws MessagingException { 330 if (isOpen() && mMode == mode) { 331 // Make sure the connection is valid. If it's not we'll close it down and continue 332 // on to get a new one. 333 try { 334 mConnection.executeSimpleCommand("NOOP"); 335 return; 336 } 337 catch (IOException ioe) { 338 ioExceptionHandler(mConnection, ioe); 339 } 340 } 341 synchronized (this) { 342 mConnection = getConnection(); 343 } 344 // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft NonJunk 345 // $MDNSent) 346 // * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft 347 // NonJunk $MDNSent \*)] Flags permitted. 348 // * 23 EXISTS 349 // * 0 RECENT 350 // * OK [UIDVALIDITY 1125022061] UIDs valid 351 // * OK [UIDNEXT 57576] Predicted next UID 352 // 2 OK [READ-WRITE] Select completed. 353 try { 354 List<ImapResponse> responses = mConnection.executeSimpleCommand( 355 String.format("SELECT \"%s\"", 356 encodeFolderName(mName))); 357 /* 358 * If the command succeeds we expect the folder has been opened read-write 359 * unless we are notified otherwise in the responses. 360 */ 361 mMode = OpenMode.READ_WRITE; 362 363 for (ImapResponse response : responses) { 364 if (response.mTag == null && response.get(1).equals("EXISTS")) { 365 mMessageCount = response.getNumber(0); 366 } 367 else if (response.mTag != null && response.size() >= 2) { 368 if ("[READ-ONLY]".equalsIgnoreCase(response.getString(1))) { 369 mMode = OpenMode.READ_ONLY; 370 } 371 else if ("[READ-WRITE]".equalsIgnoreCase(response.getString(1))) { 372 mMode = OpenMode.READ_WRITE; 373 } 374 } 375 } 376 377 if (mMessageCount == -1) { 378 throw new MessagingException( 379 "Did not find message count during select"); 380 } 381 mExists = true; 382 383 } catch (IOException ioe) { 384 throw ioExceptionHandler(mConnection, ioe); 385 } 386 } 387 388 public boolean isOpen() { 389 return mConnection != null; 390 } 391 392 @Override 393 public OpenMode getMode() throws MessagingException { 394 return mMode; 395 } 396 397 public void close(boolean expunge) { 398 if (!isOpen()) { 399 return; 400 } 401 // TODO implement expunge 402 mMessageCount = -1; 403 synchronized (this) { 404 releaseConnection(mConnection); 405 mConnection = null; 406 } 407 } 408 409 public String getName() { 410 return mName; 411 } 412 413 public boolean exists() throws MessagingException { 414 if (mExists) { 415 return true; 416 } 417 /* 418 * This method needs to operate in the unselected mode as well as the selected mode 419 * so we must get the connection ourselves if it's not there. We are specifically 420 * not calling checkOpen() since we don't care if the folder is open. 421 */ 422 ImapConnection connection = null; 423 synchronized(this) { 424 if (mConnection == null) { 425 connection = getConnection(); 426 } 427 else { 428 connection = mConnection; 429 } 430 } 431 try { 432 connection.executeSimpleCommand(String.format("STATUS \"%s\" (UIDVALIDITY)", 433 encodeFolderName(mName))); 434 mExists = true; 435 return true; 436 } 437 catch (MessagingException me) { 438 return false; 439 } 440 catch (IOException ioe) { 441 throw ioExceptionHandler(connection, ioe); 442 } 443 finally { 444 if (mConnection == null) { 445 releaseConnection(connection); 446 } 447 } 448 } 449 450 public boolean create(FolderType type) throws MessagingException { 451 /* 452 * This method needs to operate in the unselected mode as well as the selected mode 453 * so we must get the connection ourselves if it's not there. We are specifically 454 * not calling checkOpen() since we don't care if the folder is open. 455 */ 456 ImapConnection connection = null; 457 synchronized(this) { 458 if (mConnection == null) { 459 connection = getConnection(); 460 } 461 else { 462 connection = mConnection; 463 } 464 } 465 try { 466 connection.executeSimpleCommand(String.format("CREATE \"%s\"", 467 encodeFolderName(mName))); 468 return true; 469 } 470 catch (MessagingException me) { 471 return false; 472 } 473 catch (IOException ioe) { 474 throw ioExceptionHandler(mConnection, ioe); 475 } 476 finally { 477 if (mConnection == null) { 478 releaseConnection(connection); 479 } 480 } 481 } 482 483 @Override 484 public void copyMessages(Message[] messages, Folder folder, 485 MessageUpdateCallbacks callbacks) throws MessagingException { 486 checkOpen(); 487 String[] uids = new String[messages.length]; 488 for (int i = 0, count = messages.length; i < count; i++) { 489 uids[i] = messages[i].getUid(); 490 } 491 try { 492 mConnection.executeSimpleCommand(String.format("UID COPY %s \"%s\"", 493 Utility.combine(uids, ','), 494 encodeFolderName(folder.getName()))); 495 } 496 catch (IOException ioe) { 497 throw ioExceptionHandler(mConnection, ioe); 498 } 499 } 500 501 @Override 502 public int getMessageCount() { 503 return mMessageCount; 504 } 505 506 @Override 507 public int getUnreadMessageCount() throws MessagingException { 508 checkOpen(); 509 try { 510 int unreadMessageCount = 0; 511 List<ImapResponse> responses = mConnection.executeSimpleCommand( 512 String.format("STATUS \"%s\" (UNSEEN)", 513 encodeFolderName(mName))); 514 for (ImapResponse response : responses) { 515 if (response.mTag == null && response.get(0).equals("STATUS")) { 516 ImapList status = response.getList(2); 517 unreadMessageCount = status.getKeyedNumber("UNSEEN"); 518 } 519 } 520 return unreadMessageCount; 521 } 522 catch (IOException ioe) { 523 throw ioExceptionHandler(mConnection, ioe); 524 } 525 } 526 527 @Override 528 public void delete(boolean recurse) throws MessagingException { 529 throw new Error("ImapStore.delete() not yet implemented"); 530 } 531 532 @Override 533 public Message getMessage(String uid) throws MessagingException { 534 checkOpen(); 535 536 try { 537 try { 538 List<ImapResponse> responses = 539 mConnection.executeSimpleCommand(String.format("UID SEARCH UID %S", uid)); 540 for (ImapResponse response : responses) { 541 if (response.mTag == null && response.get(0).equals("SEARCH")) { 542 for (int i = 1, count = response.size(); i < count; i++) { 543 if (uid.equals(response.get(i))) { 544 return new ImapMessage(uid, this); 545 } 546 } 547 } 548 } 549 } 550 catch (MessagingException me) { 551 return null; 552 } 553 } 554 catch (IOException ioe) { 555 throw ioExceptionHandler(mConnection, ioe); 556 } 557 return null; 558 } 559 560 @Override 561 public Message[] getMessages(int start, int end, MessageRetrievalListener listener) 562 throws MessagingException { 563 if (start < 1 || end < 1 || end < start) { 564 throw new MessagingException( 565 String.format("Invalid message set %d %d", 566 start, end)); 567 } 568 checkOpen(); 569 ArrayList<Message> messages = new ArrayList<Message>(); 570 try { 571 ArrayList<String> uids = new ArrayList<String>(); 572 List<ImapResponse> responses = mConnection 573 .executeSimpleCommand(String.format("UID SEARCH %d:%d NOT DELETED", start, end)); 574 for (ImapResponse response : responses) { 575 if (response.get(0).equals("SEARCH")) { 576 for (int i = 1, count = response.size(); i < count; i++) { 577 uids.add(response.getString(i)); 578 } 579 } 580 } 581 for (int i = 0, count = uids.size(); i < count; i++) { 582 if (listener != null) { 583 listener.messageStarted(uids.get(i), i, count); 584 } 585 ImapMessage message = new ImapMessage(uids.get(i), this); 586 messages.add(message); 587 if (listener != null) { 588 listener.messageFinished(message, i, count); 589 } 590 } 591 } catch (IOException ioe) { 592 throw ioExceptionHandler(mConnection, ioe); 593 } 594 return messages.toArray(new Message[] {}); 595 } 596 597 public Message[] getMessages(MessageRetrievalListener listener) throws MessagingException { 598 return getMessages(null, listener); 599 } 600 601 public Message[] getMessages(String[] uids, MessageRetrievalListener listener) 602 throws MessagingException { 603 checkOpen(); 604 ArrayList<Message> messages = new ArrayList<Message>(); 605 try { 606 if (uids == null) { 607 List<ImapResponse> responses = mConnection 608 .executeSimpleCommand("UID SEARCH 1:* NOT DELETED"); 609 ArrayList<String> tempUids = new ArrayList<String>(); 610 for (ImapResponse response : responses) { 611 if (response.get(0).equals("SEARCH")) { 612 for (int i = 1, count = response.size(); i < count; i++) { 613 tempUids.add(response.getString(i)); 614 } 615 } 616 } 617 uids = tempUids.toArray(new String[] {}); 618 } 619 for (int i = 0, count = uids.length; i < count; i++) { 620 if (listener != null) { 621 listener.messageStarted(uids[i], i, count); 622 } 623 ImapMessage message = new ImapMessage(uids[i], this); 624 messages.add(message); 625 if (listener != null) { 626 listener.messageFinished(message, i, count); 627 } 628 } 629 } catch (IOException ioe) { 630 throw ioExceptionHandler(mConnection, ioe); 631 } 632 return messages.toArray(new Message[] {}); 633 } 634 635 public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener) 636 throws MessagingException { 637 if (messages == null || messages.length == 0) { 638 return; 639 } 640 checkOpen(); 641 String[] uids = new String[messages.length]; 642 HashMap<String, Message> messageMap = new HashMap<String, Message>(); 643 for (int i = 0, count = messages.length; i < count; i++) { 644 uids[i] = messages[i].getUid(); 645 messageMap.put(uids[i], messages[i]); 646 } 647 648 /* 649 * Figure out what command we are going to run: 650 * Flags - UID FETCH (FLAGS) 651 * Envelope - UID FETCH ([FLAGS] INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[HEADER.FIELDS (date subject from content-type to cc)]) 652 * 653 */ 654 LinkedHashSet<String> fetchFields = new LinkedHashSet<String>(); 655 fetchFields.add("UID"); 656 if (fp.contains(FetchProfile.Item.FLAGS)) { 657 fetchFields.add("FLAGS"); 658 } 659 if (fp.contains(FetchProfile.Item.ENVELOPE)) { 660 fetchFields.add("INTERNALDATE"); 661 fetchFields.add("RFC822.SIZE"); 662 fetchFields.add("BODY.PEEK[HEADER.FIELDS " + 663 "(date subject from content-type to cc message-id)]"); 664 } 665 if (fp.contains(FetchProfile.Item.STRUCTURE)) { 666 fetchFields.add("BODYSTRUCTURE"); 667 } 668 if (fp.contains(FetchProfile.Item.BODY_SANE)) { 669 fetchFields.add(String.format("BODY.PEEK[]<0.%d>", FETCH_BODY_SANE_SUGGESTED_SIZE)); 670 } 671 if (fp.contains(FetchProfile.Item.BODY)) { 672 fetchFields.add("BODY.PEEK[]"); 673 } 674 for (Object o : fp) { 675 if (o instanceof Part) { 676 Part part = (Part) o; 677 String partId = part.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA)[0]; 678 fetchFields.add("BODY.PEEK[" + partId + "]"); 679 } 680 } 681 682 try { 683 String tag = mConnection.sendCommand(String.format("UID FETCH %s (%s)", 684 Utility.combine(uids, ','), 685 Utility.combine(fetchFields.toArray(new String[fetchFields.size()]), ' ') 686 ), false); 687 ImapResponse response; 688 int messageNumber = 0; 689 do { 690 response = mConnection.readResponse(); 691 692 if (response.mTag == null && response.get(1).equals("FETCH")) { 693 ImapList fetchList = (ImapList)response.getKeyedValue("FETCH"); 694 String uid = fetchList.getKeyedString("UID"); 695 696 Message message = messageMap.get(uid); 697 698 if (listener != null) { 699 listener.messageStarted(uid, messageNumber++, messageMap.size()); 700 } 701 702 if (fp.contains(FetchProfile.Item.FLAGS)) { 703 ImapList flags = fetchList.getKeyedList("FLAGS"); 704 ImapMessage imapMessage = (ImapMessage) message; 705 if (flags != null) { 706 for (int i = 0, count = flags.size(); i < count; i++) { 707 String flag = flags.getString(i); 708 if (flag.equals("\\Deleted")) { 709 imapMessage.setFlagInternal(Flag.DELETED, true); 710 } 711 else if (flag.equals("\\Answered")) { 712 imapMessage.setFlagInternal(Flag.ANSWERED, true); 713 } 714 else if (flag.equals("\\Seen")) { 715 imapMessage.setFlagInternal(Flag.SEEN, true); 716 } 717 else if (flag.equals("\\Flagged")) { 718 imapMessage.setFlagInternal(Flag.FLAGGED, true); 719 } 720 } 721 } 722 } 723 if (fp.contains(FetchProfile.Item.ENVELOPE)) { 724 Date internalDate = fetchList.getKeyedDate("INTERNALDATE"); 725 int size = fetchList.getKeyedNumber("RFC822.SIZE"); 726 InputStream headerStream = fetchList.getLiteral(fetchList.size() - 1); 727 728 ImapMessage imapMessage = (ImapMessage) message; 729 730 message.setInternalDate(internalDate); 731 imapMessage.setSize(size); 732 imapMessage.parse(headerStream); 733 } 734 if (fp.contains(FetchProfile.Item.STRUCTURE)) { 735 ImapList bs = fetchList.getKeyedList("BODYSTRUCTURE"); 736 if (bs != null) { 737 try { 738 parseBodyStructure(bs, message, "TEXT"); 739 } 740 catch (MessagingException e) { 741 if (Email.LOGD) { 742 Log.v(Email.LOG_TAG, "Error handling message", e); 743 } 744 message.setBody(null); 745 } 746 } 747 } 748 if (fp.contains(FetchProfile.Item.BODY)) { 749 InputStream bodyStream = fetchList.getLiteral(fetchList.size() - 1); 750 ImapMessage imapMessage = (ImapMessage) message; 751 imapMessage.parse(bodyStream); 752 } 753 if (fp.contains(FetchProfile.Item.BODY_SANE)) { 754 InputStream bodyStream = fetchList.getLiteral(fetchList.size() - 1); 755 ImapMessage imapMessage = (ImapMessage) message; 756 imapMessage.parse(bodyStream); 757 } 758 for (Object o : fp) { 759 if (o instanceof Part) { 760 Part part = (Part) o; 761 InputStream bodyStream = fetchList.getLiteral(fetchList.size() - 1); 762 String contentType = part.getContentType(); 763 String contentTransferEncoding = part.getHeader( 764 MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING)[0]; 765 part.setBody(MimeUtility.decodeBody( 766 bodyStream, 767 contentTransferEncoding)); 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 CountingOutputStream out = new CountingOutputStream(); 986 EOLConvertingOutputStream eolOut = new EOLConvertingOutputStream(out); 987 message.writeTo(eolOut); 988 eolOut.flush(); 989 mConnection.sendCommand( 990 String.format("APPEND \"%s\" {%d}", 991 encodeFolderName(mName), 992 out.getCount()), false); 993 ImapResponse response; 994 do { 995 response = mConnection.readResponse(); 996 if (response.mCommandContinuationRequested) { 997 eolOut = new EOLConvertingOutputStream(mConnection.mTransport.getOutputStream()); 998 message.writeTo(eolOut); 999 eolOut.write('\r'); 1000 eolOut.write('\n'); 1001 eolOut.flush(); 1002 } 1003 else if (response.mTag == null) { 1004 handleUntaggedResponse(response); 1005 } 1006 while (response.more()); 1007 } while(response.mTag == null); 1008 1009 /* 1010 * Try to find the UID of the message we just appended using the 1011 * Message-ID header. 1012 */ 1013 String[] messageIdHeader = message.getHeader("Message-ID"); 1014 if (messageIdHeader == null || messageIdHeader.length == 0) { 1015 continue; 1016 } 1017 String messageId = messageIdHeader[0]; 1018 List<ImapResponse> responses = 1019 mConnection.executeSimpleCommand( 1020 String.format("UID SEARCH (HEADER MESSAGE-ID %s)", messageId)); 1021 for (ImapResponse response1 : responses) { 1022 if (response1.mTag == null && response1.get(0).equals("SEARCH") 1023 && response1.size() > 1) { 1024 message.setUid(response1.getString(1)); 1025 } 1026 } 1027 1028 } 1029 } 1030 catch (IOException ioe) { 1031 throw ioExceptionHandler(mConnection, ioe); 1032 } 1033 } 1034 1035 public Message[] expunge() throws MessagingException { 1036 checkOpen(); 1037 try { 1038 handleUntaggedResponses(mConnection.executeSimpleCommand("EXPUNGE")); 1039 } catch (IOException ioe) { 1040 throw ioExceptionHandler(mConnection, ioe); 1041 } 1042 return null; 1043 } 1044 1045 public void setFlags(Message[] messages, Flag[] flags, boolean value) 1046 throws MessagingException { 1047 checkOpen(); 1048 String[] uids = new String[messages.length]; 1049 for (int i = 0, count = messages.length; i < count; i++) { 1050 uids[i] = messages[i].getUid(); 1051 } 1052 ArrayList<String> flagNames = new ArrayList<String>(); 1053 for (int i = 0, count = flags.length; i < count; i++) { 1054 Flag flag = flags[i]; 1055 if (flag == Flag.SEEN) { 1056 flagNames.add("\\Seen"); 1057 } 1058 else if (flag == Flag.DELETED) { 1059 flagNames.add("\\Deleted"); 1060 } 1061 } 1062 try { 1063 mConnection.executeSimpleCommand(String.format("UID STORE %s %sFLAGS.SILENT (%s)", 1064 Utility.combine(uids, ','), 1065 value ? "+" : "-", 1066 Utility.combine(flagNames.toArray(new String[flagNames.size()]), ' '))); 1067 } 1068 catch (IOException ioe) { 1069 throw ioExceptionHandler(mConnection, ioe); 1070 } 1071 } 1072 1073 private void checkOpen() throws MessagingException { 1074 if (!isOpen()) { 1075 throw new MessagingException("Folder " + mName + " is not open."); 1076 } 1077 } 1078 1079 private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) 1080 throws MessagingException { 1081 connection.close(); 1082 close(false); 1083 return new MessagingException("IO Error", ioe); 1084 } 1085 1086 @Override 1087 public boolean equals(Object o) { 1088 if (o instanceof ImapFolder) { 1089 return ((ImapFolder)o).mName.equals(mName); 1090 } 1091 return super.equals(o); 1092 } 1093 1094 @Override 1095 public Message createMessage(String uid) throws MessagingException { 1096 return new ImapMessage(uid, this); 1097 } 1098 } 1099 1100 /** 1101 * A cacheable class that stores the details for a single IMAP connection. 1102 */ 1103 class ImapConnection { 1104 private Transport mTransport; 1105 private ImapResponseParser mParser; 1106 private int mNextCommandTag; 1107 1108 public void open() throws IOException, MessagingException { 1109 if (mTransport != null && mTransport.isOpen()) { 1110 return; 1111 } 1112 1113 mNextCommandTag = 1; 1114 1115 try { 1116 // copy configuration into a clean transport, if necessary 1117 if (mTransport == null) { 1118 mTransport = mRootTransport.newInstanceWithConfiguration(); 1119 } 1120 1121 mTransport.open(); 1122 mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT); 1123 1124 mParser = new ImapResponseParser(mTransport.getInputStream()); 1125 1126 // BANNER 1127 mParser.readResponse(); 1128 1129 if (mTransport.canTryTlsSecurity()) { 1130 // CAPABILITY 1131 List<ImapResponse> responses = executeSimpleCommand("CAPABILITY"); 1132 if (responses.size() != 2) { 1133 throw new MessagingException("Invalid CAPABILITY response received"); 1134 } 1135 if (responses.get(0).contains("STARTTLS")) { 1136 // STARTTLS 1137 executeSimpleCommand("STARTTLS"); 1138 1139 mTransport.reopenTls(); 1140 mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT); 1141 mParser = new ImapResponseParser(mTransport.getInputStream()); 1142 } else if (mTransport.getSecurity() == 1143 Transport.CONNECTION_SECURITY_TLS_REQUIRED) { 1144 if (Config.LOGD && Email.DEBUG) { 1145 Log.d(Email.LOG_TAG, "TLS not supported but required"); 1146 } 1147 throw new MessagingException(MessagingException.TLS_REQUIRED); 1148 } 1149 } 1150 1151 try { 1152 // TODO eventually we need to add additional authentication 1153 // options such as SASL 1154 executeSimpleCommand(mLoginPhrase, true); 1155 } catch (ImapException ie) { 1156 if (Config.LOGD && Email.DEBUG) { 1157 Log.d(Email.LOG_TAG, ie.toString()); 1158 } 1159 throw new AuthenticationFailedException(ie.getAlertText(), ie); 1160 1161 } catch (MessagingException me) { 1162 throw new AuthenticationFailedException(null, me); 1163 } 1164 } catch (SSLException e) { 1165 if (Config.LOGD && Email.DEBUG) { 1166 Log.d(Email.LOG_TAG, e.toString()); 1167 } 1168 throw new CertificateValidationException(e.getMessage(), e); 1169 } catch (IOException ioe) { 1170 // NOTE: Unlike similar code in POP3, I'm going to rethrow as-is. There is a lot 1171 // of other code here that catches IOException and I don't want to break it. 1172 // This catch is only here to enhance logging of connection-time issues. 1173 if (Config.LOGD && Email.DEBUG) { 1174 Log.d(Email.LOG_TAG, ioe.toString()); 1175 } 1176 throw ioe; 1177 } 1178 } 1179 1180 public void close() { 1181// if (isOpen()) { 1182// try { 1183// executeSimpleCommand("LOGOUT"); 1184// } catch (Exception e) { 1185// 1186// } 1187// } 1188 if (mTransport != null) { 1189 mTransport.close(); 1190 } 1191 } 1192 1193 public ImapResponse readResponse() throws IOException, MessagingException { 1194 return mParser.readResponse(); 1195 } 1196 1197 /** 1198 * Send a single command to the server. The command will be preceded by an IMAP command 1199 * tag and followed by \r\n (caller need not supply them). 1200 * 1201 * @param command The command to send to the server 1202 * @param sensitive If true, the command will not be logged 1203 * @return Returns the command tag that was sent 1204 */ 1205 public String sendCommand(String command, boolean sensitive) 1206 throws MessagingException, IOException { 1207 open(); 1208 String tag = Integer.toString(mNextCommandTag++); 1209 String commandToSend = tag + " " + command; 1210 mTransport.writeLine(commandToSend, sensitive ? "[IMAP command redacted]" : null); 1211 return tag; 1212 } 1213 1214 public List<ImapResponse> executeSimpleCommand(String command) throws IOException, 1215 ImapException, MessagingException { 1216 return executeSimpleCommand(command, false); 1217 } 1218 1219 public List<ImapResponse> executeSimpleCommand(String command, boolean sensitive) 1220 throws IOException, ImapException, MessagingException { 1221 String tag = sendCommand(command, sensitive); 1222 ArrayList<ImapResponse> responses = new ArrayList<ImapResponse>(); 1223 ImapResponse response; 1224 ImapResponse previous = null; 1225 do { 1226 // This is work around to parse literal in the middle of response. 1227 // We should nail down the previous response literal string if any. 1228 if (previous != null && !previous.completed()) { 1229 previous.nailDown(); 1230 } 1231 response = mParser.readResponse(); 1232 // This is work around to parse literal in the middle of response. 1233 // If we found unmatched tagged response, it possibly be the continuous 1234 // response just after the literal string. 1235 if (response.mTag != null && !response.mTag.equals(tag) 1236 && previous != null && !previous.completed()) { 1237 previous.appendAll(response); 1238 response.mTag = null; 1239 continue; 1240 } 1241 responses.add(response); 1242 previous = response; 1243 } while (response.mTag == null); 1244 if (response.size() < 1 || !response.get(0).equals("OK")) { 1245 throw new ImapException(response.toString(), response.getAlertText()); 1246 } 1247 return responses; 1248 } 1249 } 1250 1251 class ImapMessage extends MimeMessage { 1252 ImapMessage(String uid, Folder folder) throws MessagingException { 1253 this.mUid = uid; 1254 this.mFolder = folder; 1255 } 1256 1257 public void setSize(int size) { 1258 this.mSize = size; 1259 } 1260 1261 public void parse(InputStream in) throws IOException, MessagingException { 1262 super.parse(in); 1263 } 1264 1265 public void setFlagInternal(Flag flag, boolean set) throws MessagingException { 1266 super.setFlag(flag, set); 1267 } 1268 1269 @Override 1270 public void setFlag(Flag flag, boolean set) throws MessagingException { 1271 super.setFlag(flag, set); 1272 mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set); 1273 } 1274 } 1275 1276 class ImapBodyPart extends MimeBodyPart { 1277 public ImapBodyPart() throws MessagingException { 1278 super(); 1279 } 1280 1281 public void setSize(int size) { 1282 this.mSize = size; 1283 } 1284 } 1285 1286 class ImapException extends MessagingException { 1287 String mAlertText; 1288 1289 public ImapException(String message, String alertText, Throwable throwable) { 1290 super(message, throwable); 1291 this.mAlertText = alertText; 1292 } 1293 1294 public ImapException(String message, String alertText) { 1295 super(message); 1296 this.mAlertText = alertText; 1297 } 1298 1299 public String getAlertText() { 1300 return mAlertText; 1301 } 1302 1303 public void setAlertText(String alertText) { 1304 mAlertText = alertText; 1305 } 1306 } 1307} 1308