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