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