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