Pop3Store.java revision e87ff6c3cbbfc5e3636f9827b58820652e3ea1c5
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.mail.Store; 21import com.android.email.mail.Transport; 22import com.android.email.mail.transport.MailTransport; 23import com.android.emailcommon.Logging; 24import com.android.emailcommon.internet.MimeMessage; 25import com.android.emailcommon.mail.AuthenticationFailedException; 26import com.android.emailcommon.mail.FetchProfile; 27import com.android.emailcommon.mail.Flag; 28import com.android.emailcommon.mail.Folder; 29import com.android.emailcommon.mail.Message; 30import com.android.emailcommon.mail.MessagingException; 31import com.android.emailcommon.mail.Folder.OpenMode; 32import com.android.emailcommon.provider.EmailContent.Account; 33import com.android.emailcommon.provider.EmailContent.HostAuth; 34import com.android.emailcommon.provider.EmailContent.Mailbox; 35import com.android.emailcommon.service.EmailServiceProxy; 36import com.android.emailcommon.utility.LoggingInputStream; 37import com.android.emailcommon.utility.Utility; 38 39import android.content.Context; 40import android.os.Bundle; 41import android.util.Log; 42 43import java.io.IOException; 44import java.io.InputStream; 45import java.util.ArrayList; 46import java.util.HashMap; 47import java.util.HashSet; 48 49public class Pop3Store extends Store { 50 // All flags defining debug or development code settings must be FALSE 51 // when code is checked in or released. 52 private static boolean DEBUG_FORCE_SINGLE_LINE_UIDL = false; 53 private static boolean DEBUG_LOG_RAW_STREAM = false; 54 55 private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED }; 56 /** The name of the only mailbox available to POP3 accounts */ 57 private static final String POP3_MAILBOX_NAME = "INBOX"; 58 private final Context mContext; 59 private final Account mAccount; 60 private Transport mTransport; 61 private String mUsername; 62 private String mPassword; 63 private final HashMap<String, Folder> mFolders = new HashMap<String, Folder>(); 64 65// /** 66// * Detected latency, used for usage scaling. 67// * Usage scaling occurs when it is neccesary to get information about 68// * messages that could result in large data loads. This value allows 69// * the code that loads this data to decide between using large downloads 70// * (high latency) or multiple round trips (low latency) to accomplish 71// * the same thing. 72// * Default is Integer.MAX_VALUE implying massive latency so that the large 73// * download method is used by default until latency data is collected. 74// */ 75// private int mLatencyMs = Integer.MAX_VALUE; 76// 77// /** 78// * Detected throughput, used for usage scaling. 79// * Usage scaling occurs when it is neccesary to get information about 80// * messages that could result in large data loads. This value allows 81// * the code that loads this data to decide between using large downloads 82// * (high latency) or multiple round trips (low latency) to accomplish 83// * the same thing. 84// * Default is Integer.MAX_VALUE implying massive bandwidth so that the 85// * large download method is used by default until latency data is 86// * collected. 87// */ 88// private int mThroughputKbS = Integer.MAX_VALUE; 89 90 /** 91 * Static named constructor. 92 */ 93 public static Store newInstance(Account account, Context context, 94 PersistentDataCallbacks callbacks) throws MessagingException { 95 return new Pop3Store(context, account); 96 } 97 98 /** 99 * Creates a new store for the given account. 100 */ 101 private Pop3Store(Context context, Account account) throws MessagingException { 102 mContext = context; 103 mAccount = account; 104 105 HostAuth recvAuth = account.getOrCreateHostAuthRecv(context); 106 if (recvAuth == null || !STORE_SCHEME_POP3.equalsIgnoreCase(recvAuth.mProtocol)) { 107 throw new MessagingException("Unsupported protocol"); 108 } 109 // defaults, which can be changed by security modifiers 110 int connectionSecurity = Transport.CONNECTION_SECURITY_NONE; 111 int defaultPort = 110; 112 113 // check for security flags and apply changes 114 if ((recvAuth.mFlags & HostAuth.FLAG_SSL) != 0) { 115 connectionSecurity = Transport.CONNECTION_SECURITY_SSL; 116 defaultPort = 995; 117 } else if ((recvAuth.mFlags & HostAuth.FLAG_TLS) != 0) { 118 connectionSecurity = Transport.CONNECTION_SECURITY_TLS; 119 } 120 boolean trustCertificates = ((recvAuth.mFlags & HostAuth.FLAG_TRUST_ALL) != 0); 121 122 int port = defaultPort; 123 if (recvAuth.mPort != HostAuth.PORT_UNKNOWN) { 124 port = recvAuth.mPort; 125 } 126 mTransport = new MailTransport("POP3"); 127 mTransport.setHost(recvAuth.mAddress); 128 mTransport.setPort(port); 129 mTransport.setSecurity(connectionSecurity, trustCertificates); 130 131 String[] userInfoParts = recvAuth.getLogin(); 132 if (userInfoParts != null) { 133 mUsername = userInfoParts[0]; 134 mPassword = userInfoParts[1]; 135 } 136 } 137 138 /** 139 * For testing only. Injects a different transport. The transport should already be set 140 * up and ready to use. Do not use for real code. 141 * @param testTransport The Transport to inject and use for all future communication. 142 */ 143 /* package */ void setTransport(Transport testTransport) { 144 mTransport = testTransport; 145 } 146 147 @Override 148 public Folder getFolder(String name) { 149 Folder folder = mFolders.get(name); 150 if (folder == null) { 151 folder = new Pop3Folder(name); 152 mFolders.put(folder.getName(), folder); 153 } 154 return folder; 155 } 156 157 @Override 158 public Folder[] updateFolders() { 159 Mailbox mailbox = getMailboxForPath(mContext, mAccount.mId, POP3_MAILBOX_NAME); 160 updateMailbox(mailbox, mAccount.mId, POP3_MAILBOX_NAME, '\0', true, Mailbox.TYPE_INBOX); 161 if (mailbox.isSaved()) { 162 mailbox.update(mContext, mailbox.toContentValues()); 163 } else { 164 mailbox.save(mContext); 165 } 166 return new Folder[] { getFolder(POP3_MAILBOX_NAME) }; 167 } 168 169 /** 170 * Used by account setup to test if an account's settings are appropriate. The definition 171 * of "checked" here is simply, can you log into the account and does it meet some minimum set 172 * of feature requirements? 173 * 174 * @throws MessagingException if there was some problem with the account 175 */ 176 @Override 177 public Bundle checkSettings() throws MessagingException { 178 Pop3Folder folder = new Pop3Folder(POP3_MAILBOX_NAME); 179 Bundle bundle = null; 180 // Close any open or half-open connections - checkSettings should always be "fresh" 181 if (mTransport.isOpen()) { 182 folder.close(false); 183 } 184 try { 185 folder.open(OpenMode.READ_WRITE, null); 186 bundle = folder.checkSettings(); 187 } finally { 188 folder.close(false); // false == don't expunge anything 189 } 190 return bundle; 191 } 192 193 class Pop3Folder extends Folder { 194 private final HashMap<String, Pop3Message> mUidToMsgMap 195 = new HashMap<String, Pop3Message>(); 196 private final HashMap<Integer, Pop3Message> mMsgNumToMsgMap 197 = new HashMap<Integer, Pop3Message>(); 198 private final HashMap<String, Integer> mUidToMsgNumMap = new HashMap<String, Integer>(); 199 private final String mName; 200 private int mMessageCount; 201 private Pop3Capabilities mCapabilities; 202 203 public Pop3Folder(String name) { 204 if (name.equalsIgnoreCase(POP3_MAILBOX_NAME)) { 205 mName = POP3_MAILBOX_NAME; 206 } else { 207 mName = name; 208 } 209 } 210 211 /** 212 * Used by account setup to test if an account's settings are appropriate. Here, we run 213 * an additional test to see if UIDL is supported on the server. If it's not we 214 * can't service this account. 215 * 216 * @return Bundle containing validation data (code and, if appropriate, error message) 217 * @throws MessagingException if the account is not going to be useable 218 */ 219 public Bundle checkSettings() throws MessagingException { 220 Bundle bundle = new Bundle(); 221 int result = MessagingException.NO_ERROR; 222 if (!mCapabilities.uidl) { 223 try { 224 UidlParser parser = new UidlParser(); 225 executeSimpleCommand("UIDL"); 226 // drain the entire output, so additional communications don't get confused. 227 String response; 228 while ((response = mTransport.readLine()) != null) { 229 parser.parseMultiLine(response); 230 if (parser.mEndOfMessage) { 231 break; 232 } 233 } 234 } catch (IOException ioe) { 235 mTransport.close(); 236 result = MessagingException.IOERROR; 237 bundle.putString(EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE, 238 ioe.getMessage()); 239 } 240 } 241 bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, result); 242 return bundle; 243 } 244 245 @Override 246 public synchronized void open(OpenMode mode, PersistentDataCallbacks callbacks) 247 throws MessagingException { 248 if (mTransport.isOpen()) { 249 return; 250 } 251 252 if (!mName.equalsIgnoreCase(POP3_MAILBOX_NAME)) { 253 throw new MessagingException("Folder does not exist"); 254 } 255 256 try { 257 mTransport.open(); 258 259 // Eat the banner 260 executeSimpleCommand(null); 261 262 mCapabilities = getCapabilities(); 263 264 if (mTransport.canTryTlsSecurity()) { 265 if (mCapabilities.stls) { 266 executeSimpleCommand("STLS"); 267 mTransport.reopenTls(); 268 } else { 269 if (Email.DEBUG) { 270 Log.d(Logging.LOG_TAG, "TLS not supported but required"); 271 } 272 throw new MessagingException(MessagingException.TLS_REQUIRED); 273 } 274 } 275 276 try { 277 executeSensitiveCommand("USER " + mUsername, "USER /redacted/"); 278 executeSensitiveCommand("PASS " + mPassword, "PASS /redacted/"); 279 } catch (MessagingException me) { 280 if (Email.DEBUG) { 281 Log.d(Logging.LOG_TAG, me.toString()); 282 } 283 throw new AuthenticationFailedException(null, me); 284 } 285 } catch (IOException ioe) { 286 mTransport.close(); 287 if (Email.DEBUG) { 288 Log.d(Logging.LOG_TAG, ioe.toString()); 289 } 290 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 291 } 292 293 Exception statException = null; 294 try { 295 String response = executeSimpleCommand("STAT"); 296 String[] parts = response.split(" "); 297 if (parts.length < 2) { 298 statException = new IOException(); 299 } else { 300 mMessageCount = Integer.parseInt(parts[1]); 301 } 302 } catch (IOException ioe) { 303 statException = ioe; 304 } catch (NumberFormatException nfe) { 305 statException = nfe; 306 } 307 if (statException != null) { 308 mTransport.close(); 309 if (Email.DEBUG) { 310 Log.d(Logging.LOG_TAG, statException.toString()); 311 } 312 throw new MessagingException("POP3 STAT", statException); 313 } 314 mUidToMsgMap.clear(); 315 mMsgNumToMsgMap.clear(); 316 mUidToMsgNumMap.clear(); 317 } 318 319 @Override 320 public OpenMode getMode() { 321 return OpenMode.READ_WRITE; 322 } 323 324 /** 325 * Close the folder (and the transport below it). 326 * 327 * MUST NOT return any exceptions. 328 * 329 * @param expunge If true all deleted messages will be expunged (TODO - not implemented) 330 */ 331 @Override 332 public void close(boolean expunge) { 333 try { 334 executeSimpleCommand("QUIT"); 335 } 336 catch (Exception e) { 337 // ignore any problems here - just continue closing 338 } 339 mTransport.close(); 340 } 341 342 @Override 343 public String getName() { 344 return mName; 345 } 346 347 // POP3 does not folder creation 348 @Override 349 public boolean canCreate(FolderType type) { 350 return false; 351 } 352 353 @Override 354 public boolean create(FolderType type) { 355 return false; 356 } 357 358 @Override 359 public boolean exists() { 360 return mName.equalsIgnoreCase(POP3_MAILBOX_NAME); 361 } 362 363 @Override 364 public int getMessageCount() { 365 return mMessageCount; 366 } 367 368 @Override 369 public int getUnreadMessageCount() { 370 return -1; 371 } 372 373 @Override 374 public Message getMessage(String uid) throws MessagingException { 375 if (mUidToMsgNumMap.size() == 0) { 376 try { 377 indexMsgNums(1, mMessageCount); 378 } catch (IOException ioe) { 379 mTransport.close(); 380 if (Email.DEBUG) { 381 Log.d(Logging.LOG_TAG, "Unable to index during getMessage " + ioe); 382 } 383 throw new MessagingException("getMessages", ioe); 384 } 385 } 386 Pop3Message message = mUidToMsgMap.get(uid); 387 return message; 388 } 389 390 @Override 391 public Message[] getMessages(int start, int end, MessageRetrievalListener listener) 392 throws MessagingException { 393 if (start < 1 || end < 1 || end < start) { 394 throw new MessagingException(String.format("Invalid message set %d %d", 395 start, end)); 396 } 397 try { 398 indexMsgNums(start, end); 399 } catch (IOException ioe) { 400 mTransport.close(); 401 if (Email.DEBUG) { 402 Log.d(Logging.LOG_TAG, ioe.toString()); 403 } 404 throw new MessagingException("getMessages", ioe); 405 } 406 ArrayList<Message> messages = new ArrayList<Message>(); 407 for (int msgNum = start; msgNum <= end; msgNum++) { 408 Pop3Message message = mMsgNumToMsgMap.get(msgNum); 409 messages.add(message); 410 if (listener != null) { 411 listener.messageRetrieved(message); 412 } 413 } 414 return messages.toArray(new Message[messages.size()]); 415 } 416 417 /** 418 * Ensures that the given message set (from start to end inclusive) 419 * has been queried so that uids are available in the local cache. 420 * @param start 421 * @param end 422 * @throws MessagingException 423 * @throws IOException 424 */ 425 private void indexMsgNums(int start, int end) 426 throws MessagingException, IOException { 427 int unindexedMessageCount = 0; 428 for (int msgNum = start; msgNum <= end; msgNum++) { 429 if (mMsgNumToMsgMap.get(msgNum) == null) { 430 unindexedMessageCount++; 431 } 432 } 433 if (unindexedMessageCount == 0) { 434 return; 435 } 436 UidlParser parser = new UidlParser(); 437 if (DEBUG_FORCE_SINGLE_LINE_UIDL || 438 (unindexedMessageCount < 50 && mMessageCount > 5000)) { 439 /* 440 * In extreme cases we'll do a UIDL command per message instead of a bulk 441 * download. 442 */ 443 for (int msgNum = start; msgNum <= end; msgNum++) { 444 Pop3Message message = mMsgNumToMsgMap.get(msgNum); 445 if (message == null) { 446 String response = executeSimpleCommand("UIDL " + msgNum); 447 if (!parser.parseSingleLine(response)) { 448 throw new IOException(); 449 } 450 message = new Pop3Message(parser.mUniqueId, this); 451 indexMessage(msgNum, message); 452 } 453 } 454 } else { 455 String response = executeSimpleCommand("UIDL"); 456 while ((response = mTransport.readLine()) != null) { 457 if (!parser.parseMultiLine(response)) { 458 throw new IOException(); 459 } 460 if (parser.mEndOfMessage) { 461 break; 462 } 463 int msgNum = parser.mMessageNumber; 464 if (msgNum >= start && msgNum <= end) { 465 Pop3Message message = mMsgNumToMsgMap.get(msgNum); 466 if (message == null) { 467 message = new Pop3Message(parser.mUniqueId, this); 468 indexMessage(msgNum, message); 469 } 470 } 471 } 472 } 473 } 474 475 private void indexUids(ArrayList<String> uids) 476 throws MessagingException, IOException { 477 HashSet<String> unindexedUids = new HashSet<String>(); 478 for (String uid : uids) { 479 if (mUidToMsgMap.get(uid) == null) { 480 unindexedUids.add(uid); 481 } 482 } 483 if (unindexedUids.size() == 0) { 484 return; 485 } 486 /* 487 * If we are missing uids in the cache the only sure way to 488 * get them is to do a full UIDL list. A possible optimization 489 * would be trying UIDL for the latest X messages and praying. 490 */ 491 UidlParser parser = new UidlParser(); 492 String response = executeSimpleCommand("UIDL"); 493 while ((response = mTransport.readLine()) != null) { 494 parser.parseMultiLine(response); 495 if (parser.mEndOfMessage) { 496 break; 497 } 498 if (unindexedUids.contains(parser.mUniqueId)) { 499 Pop3Message message = mUidToMsgMap.get(parser.mUniqueId); 500 if (message == null) { 501 message = new Pop3Message(parser.mUniqueId, this); 502 } 503 indexMessage(parser.mMessageNumber, message); 504 } 505 } 506 } 507 508 /** 509 * Simple parser class for UIDL messages. 510 * 511 * <p>NOTE: In variance with RFC 1939, we allow multiple whitespace between the 512 * message-number and unique-id fields. This provides greater compatibility with some 513 * non-compliant POP3 servers, e.g. mail.comcast.net. 514 */ 515 /* package */ class UidlParser { 516 517 /** 518 * Caller can read back message-number from this field 519 */ 520 public int mMessageNumber; 521 /** 522 * Caller can read back unique-id from this field 523 */ 524 public String mUniqueId; 525 /** 526 * True if the response was "end-of-message" 527 */ 528 public boolean mEndOfMessage; 529 /** 530 * True if an error was reported 531 */ 532 public boolean mErr; 533 534 /** 535 * Construct & Initialize 536 */ 537 public UidlParser() { 538 mErr = true; 539 } 540 541 /** 542 * Parse a single-line response. This is returned from a command of the form 543 * "UIDL msg-num" and will be formatted as: "+OK msg-num unique-id" or 544 * "-ERR diagnostic text" 545 * 546 * @param response The string returned from the server 547 * @return true if the string parsed as expected (e.g. no syntax problems) 548 */ 549 public boolean parseSingleLine(String response) { 550 mErr = false; 551 if (response == null || response.length() == 0) { 552 return false; 553 } 554 char first = response.charAt(0); 555 if (first == '+') { 556 String[] uidParts = response.split(" +"); 557 if (uidParts.length >= 3) { 558 try { 559 mMessageNumber = Integer.parseInt(uidParts[1]); 560 } catch (NumberFormatException nfe) { 561 return false; 562 } 563 mUniqueId = uidParts[2]; 564 mEndOfMessage = true; 565 return true; 566 } 567 } else if (first == '-') { 568 mErr = true; 569 return true; 570 } 571 return false; 572 } 573 574 /** 575 * Parse a multi-line response. This is returned from a command of the form 576 * "UIDL" and will be formatted as: "." or "msg-num unique-id". 577 * 578 * @param response The string returned from the server 579 * @return true if the string parsed as expected (e.g. no syntax problems) 580 */ 581 public boolean parseMultiLine(String response) { 582 mErr = false; 583 if (response == null || response.length() == 0) { 584 return false; 585 } 586 char first = response.charAt(0); 587 if (first == '.') { 588 mEndOfMessage = true; 589 return true; 590 } else { 591 String[] uidParts = response.split(" +"); 592 if (uidParts.length >= 2) { 593 try { 594 mMessageNumber = Integer.parseInt(uidParts[0]); 595 } catch (NumberFormatException nfe) { 596 return false; 597 } 598 mUniqueId = uidParts[1]; 599 mEndOfMessage = false; 600 return true; 601 } 602 } 603 return false; 604 } 605 } 606 607 private void indexMessage(int msgNum, Pop3Message message) { 608 mMsgNumToMsgMap.put(msgNum, message); 609 mUidToMsgMap.put(message.getUid(), message); 610 mUidToMsgNumMap.put(message.getUid(), msgNum); 611 } 612 613 @Override 614 public Message[] getMessages(MessageRetrievalListener listener) { 615 throw new UnsupportedOperationException( 616 "Pop3Folder.getMessage(MessageRetrievalListener)"); 617 } 618 619 @Override 620 public Message[] getMessages(String[] uids, MessageRetrievalListener listener) { 621 throw new UnsupportedOperationException( 622 "Pop3Folder.getMessage(MessageRetrievalListener)"); 623 } 624 625 /** 626 * Fetch the items contained in the FetchProfile into the given set of 627 * Messages in as efficient a manner as possible. 628 * @param messages 629 * @param fp 630 * @throws MessagingException 631 */ 632 @Override 633 public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener) 634 throws MessagingException { 635 if (messages == null || messages.length == 0) { 636 return; 637 } 638 ArrayList<String> uids = new ArrayList<String>(); 639 for (Message message : messages) { 640 uids.add(message.getUid()); 641 } 642 try { 643 indexUids(uids); 644 if (fp.contains(FetchProfile.Item.ENVELOPE)) { 645 // Note: We never pass the listener for the ENVELOPE call, because we're going 646 // to be calling the listener below in the per-message loop. 647 fetchEnvelope(messages, null); 648 } 649 } catch (IOException ioe) { 650 mTransport.close(); 651 if (Email.DEBUG) { 652 Log.d(Logging.LOG_TAG, ioe.toString()); 653 } 654 throw new MessagingException("fetch", ioe); 655 } 656 for (int i = 0, count = messages.length; i < count; i++) { 657 Message message = messages[i]; 658 if (!(message instanceof Pop3Message)) { 659 throw new MessagingException("Pop3Store.fetch called with non-Pop3 Message"); 660 } 661 Pop3Message pop3Message = (Pop3Message)message; 662 try { 663 if (fp.contains(FetchProfile.Item.BODY)) { 664 fetchBody(pop3Message, -1); 665 } 666 else if (fp.contains(FetchProfile.Item.BODY_SANE)) { 667 /* 668 * To convert the suggested download size we take the size 669 * divided by the maximum line size (76). 670 */ 671 fetchBody(pop3Message, 672 FETCH_BODY_SANE_SUGGESTED_SIZE / 76); 673 } 674 else if (fp.contains(FetchProfile.Item.STRUCTURE)) { 675 /* 676 * If the user is requesting STRUCTURE we are required to set the body 677 * to null since we do not support the function. 678 */ 679 pop3Message.setBody(null); 680 } 681 if (listener != null) { 682 listener.messageRetrieved(message); 683 } 684 } catch (IOException ioe) { 685 mTransport.close(); 686 if (Email.DEBUG) { 687 Log.d(Logging.LOG_TAG, ioe.toString()); 688 } 689 throw new MessagingException("Unable to fetch message", ioe); 690 } 691 } 692 } 693 694 private void fetchEnvelope(Message[] messages, 695 MessageRetrievalListener listener) throws IOException, MessagingException { 696 int unsizedMessages = 0; 697 for (Message message : messages) { 698 if (message.getSize() == -1) { 699 unsizedMessages++; 700 } 701 } 702 if (unsizedMessages == 0) { 703 return; 704 } 705 if (unsizedMessages < 50 && mMessageCount > 5000) { 706 /* 707 * In extreme cases we'll do a command per message instead of a bulk request 708 * to hopefully save some time and bandwidth. 709 */ 710 for (int i = 0, count = messages.length; i < count; i++) { 711 Message message = messages[i]; 712 if (!(message instanceof Pop3Message)) { 713 throw new MessagingException("Pop3Store.fetch called with non-Pop3 Message"); 714 } 715 Pop3Message pop3Message = (Pop3Message)message; 716 String response = executeSimpleCommand(String.format("LIST %d", 717 mUidToMsgNumMap.get(pop3Message.getUid()))); 718 try { 719 String[] listParts = response.split(" "); 720 int msgNum = Integer.parseInt(listParts[1]); 721 int msgSize = Integer.parseInt(listParts[2]); 722 pop3Message.setSize(msgSize); 723 } catch (NumberFormatException nfe) { 724 throw new IOException(); 725 } 726 if (listener != null) { 727 listener.messageRetrieved(pop3Message); 728 } 729 } 730 } else { 731 HashSet<String> msgUidIndex = new HashSet<String>(); 732 for (Message message : messages) { 733 msgUidIndex.add(message.getUid()); 734 } 735 String response = executeSimpleCommand("LIST"); 736 while ((response = mTransport.readLine()) != null) { 737 if (response.equals(".")) { 738 break; 739 } 740 Pop3Message pop3Message = null; 741 int msgSize = 0; 742 try { 743 String[] listParts = response.split(" "); 744 int msgNum = Integer.parseInt(listParts[0]); 745 msgSize = Integer.parseInt(listParts[1]); 746 pop3Message = mMsgNumToMsgMap.get(msgNum); 747 } catch (NumberFormatException nfe) { 748 throw new IOException(); 749 } 750 if (pop3Message != null && msgUidIndex.contains(pop3Message.getUid())) { 751 pop3Message.setSize(msgSize); 752 if (listener != null) { 753 listener.messageRetrieved(pop3Message); 754 } 755 } 756 } 757 } 758 } 759 760 /** 761 * Fetches the body of the given message, limiting the stored data 762 * to the specified number of lines. If lines is -1 the entire message 763 * is fetched. This is implemented with RETR for lines = -1 or TOP 764 * for any other value. If the server does not support TOP it is 765 * emulated with RETR and extra lines are thrown away. 766 * 767 * Note: Some servers (e.g. live.com) don't support CAPA, but turn out to 768 * support TOP after all. For better performance on these servers, we'll always 769 * probe TOP, and fall back to RETR when it's truly unsupported. 770 * 771 * @param message 772 * @param lines 773 */ 774 private void fetchBody(Pop3Message message, int lines) 775 throws IOException, MessagingException { 776 String response = null; 777 int messageId = mUidToMsgNumMap.get(message.getUid()); 778 if (lines == -1) { 779 // Fetch entire message 780 response = executeSimpleCommand(String.format("RETR %d", messageId)); 781 } else { 782 // Fetch partial message. Try "TOP", and fall back to slower "RETR" if necessary 783 try { 784 response = executeSimpleCommand(String.format("TOP %d %d", messageId, lines)); 785 } catch (MessagingException me) { 786 response = executeSimpleCommand(String.format("RETR %d", messageId)); 787 } 788 } 789 if (response != null) { 790 try { 791 InputStream in = mTransport.getInputStream(); 792 if (DEBUG_LOG_RAW_STREAM && Email.DEBUG) { 793 in = new LoggingInputStream(in); 794 } 795 message.parse(new Pop3ResponseInputStream(in)); 796 } 797 catch (MessagingException me) { 798 /* 799 * If we're only downloading headers it's possible 800 * we'll get a broken MIME message which we're not 801 * real worried about. If we've downloaded the body 802 * and can't parse it we need to let the user know. 803 */ 804 if (lines == -1) { 805 throw me; 806 } 807 } 808 } 809 } 810 811 @Override 812 public Flag[] getPermanentFlags() { 813 return PERMANENT_FLAGS; 814 } 815 816 @Override 817 public void appendMessages(Message[] messages) { 818 } 819 820 @Override 821 public void delete(boolean recurse) { 822 } 823 824 @Override 825 public Message[] expunge() { 826 return null; 827 } 828 829 @Override 830 public void setFlags(Message[] messages, Flag[] flags, boolean value) 831 throws MessagingException { 832 if (!value || !Utility.arrayContains(flags, Flag.DELETED)) { 833 /* 834 * The only flagging we support is setting the Deleted flag. 835 */ 836 return; 837 } 838 try { 839 for (Message message : messages) { 840 executeSimpleCommand(String.format("DELE %s", 841 mUidToMsgNumMap.get(message.getUid()))); 842 } 843 } 844 catch (IOException ioe) { 845 mTransport.close(); 846 if (Email.DEBUG) { 847 Log.d(Logging.LOG_TAG, ioe.toString()); 848 } 849 throw new MessagingException("setFlags()", ioe); 850 } 851 } 852 853 @Override 854 public void copyMessages(Message[] msgs, Folder folder, MessageUpdateCallbacks callbacks) { 855 throw new UnsupportedOperationException("copyMessages is not supported in POP3"); 856 } 857 858// private boolean isRoundTripModeSuggested() { 859// long roundTripMethodMs = 860// (uncachedMessageCount * 2 * mLatencyMs); 861// long bulkMethodMs = 862// (mMessageCount * 58) / (mThroughputKbS * 1024 / 8) * 1000; 863// } 864 865 private Pop3Capabilities getCapabilities() throws IOException { 866 Pop3Capabilities capabilities = new Pop3Capabilities(); 867 try { 868 String response = executeSimpleCommand("CAPA"); 869 while ((response = mTransport.readLine()) != null) { 870 if (response.equals(".")) { 871 break; 872 } 873 if (response.equalsIgnoreCase("STLS")){ 874 capabilities.stls = true; 875 } 876 else if (response.equalsIgnoreCase("UIDL")) { 877 capabilities.uidl = true; 878 } 879 else if (response.equalsIgnoreCase("PIPELINING")) { 880 capabilities.pipelining = true; 881 } 882 else if (response.equalsIgnoreCase("USER")) { 883 capabilities.user = true; 884 } 885 else if (response.equalsIgnoreCase("TOP")) { 886 capabilities.top = true; 887 } 888 } 889 } 890 catch (MessagingException me) { 891 /* 892 * The server may not support the CAPA command, so we just eat this Exception 893 * and allow the empty capabilities object to be returned. 894 */ 895 } 896 return capabilities; 897 } 898 899 /** 900 * Send a single command and wait for a single line response. Reopens the connection, 901 * if it is closed. Leaves the connection open. 902 * 903 * @param command The command string to send to the server. 904 * @return Returns the response string from the server. 905 */ 906 private String executeSimpleCommand(String command) throws IOException, MessagingException { 907 return executeSensitiveCommand(command, null); 908 } 909 910 /** 911 * Send a single command and wait for a single line response. Reopens the connection, 912 * if it is closed. Leaves the connection open. 913 * 914 * @param command The command string to send to the server. 915 * @param sensitiveReplacement If the command includes sensitive data (e.g. authentication) 916 * please pass a replacement string here (for logging). 917 * @return Returns the response string from the server. 918 */ 919 private String executeSensitiveCommand(String command, String sensitiveReplacement) 920 throws IOException, MessagingException { 921 open(OpenMode.READ_WRITE, null); 922 923 if (command != null) { 924 mTransport.writeLine(command, sensitiveReplacement); 925 } 926 927 String response = mTransport.readLine(); 928 929 if (response.length() > 1 && response.charAt(0) == '-') { 930 throw new MessagingException(response); 931 } 932 933 return response; 934 } 935 936 @Override 937 public boolean equals(Object o) { 938 if (o instanceof Pop3Folder) { 939 return ((Pop3Folder) o).mName.equals(mName); 940 } 941 return super.equals(o); 942 } 943 944 @Override 945 public boolean isOpenForTest() { 946 return mTransport.isOpen(); 947 } 948 949 @Override 950 public Message createMessage(String uid) { 951 return new Pop3Message(uid, this); 952 } 953 } 954 955 public static class Pop3Message extends MimeMessage { 956 public Pop3Message(String uid, Pop3Folder folder) { 957 mUid = uid; 958 mFolder = folder; 959 mSize = -1; 960 } 961 962 public void setSize(int size) { 963 mSize = size; 964 } 965 966 @Override 967 public void parse(InputStream in) throws IOException, MessagingException { 968 super.parse(in); 969 } 970 971 @Override 972 public void setFlag(Flag flag, boolean set) throws MessagingException { 973 super.setFlag(flag, set); 974 mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set); 975 } 976 } 977 978 /** 979 * POP3 Capabilities as defined in RFC 2449. This is not a complete list of CAPA 980 * responses - just those that we use in this client. 981 */ 982 class Pop3Capabilities { 983 /** The STLS (start TLS) command is supported */ 984 public boolean stls; 985 /** the TOP command (retrieve a partial message) is supported */ 986 public boolean top; 987 /** USER and PASS login/auth commands are supported */ 988 public boolean user; 989 /** the optional UIDL command is supported (unused) */ 990 public boolean uidl; 991 /** the server is capable of accepting multiple commands at a time (unused) */ 992 public boolean pipelining; 993 994 @Override 995 public String toString() { 996 return String.format("STLS %b, TOP %b, USER %b, UIDL %b, PIPELINING %b", 997 stls, 998 top, 999 user, 1000 uidl, 1001 pipelining); 1002 } 1003 } 1004 1005 // TODO figure out what is special about this and merge it into MailTransport 1006 class Pop3ResponseInputStream extends InputStream { 1007 private final InputStream mIn; 1008 private boolean mStartOfLine = true; 1009 private boolean mFinished; 1010 1011 public Pop3ResponseInputStream(InputStream in) { 1012 mIn = in; 1013 } 1014 1015 @Override 1016 public int read() throws IOException { 1017 if (mFinished) { 1018 return -1; 1019 } 1020 int d = mIn.read(); 1021 if (mStartOfLine && d == '.') { 1022 d = mIn.read(); 1023 if (d == '\r') { 1024 mFinished = true; 1025 mIn.read(); 1026 return -1; 1027 } 1028 } 1029 1030 mStartOfLine = (d == '\n'); 1031 1032 return d; 1033 } 1034 } 1035} 1036