Pop3Store.java revision 32311cce0153fbb2708d871626a0797cc93b7e4e
1/* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.email.mail.store; 18 19import com.android.email.Email; 20import com.android.email.Utility; 21import com.android.email.mail.Store; 22import com.android.email.mail.Transport; 23import com.android.email.mail.transport.LoggingInputStream; 24import com.android.email.mail.transport.MailTransport; 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.service.EmailServiceProxy; 34 35import android.content.Context; 36import android.os.Bundle; 37import android.util.Config; 38import android.util.Log; 39 40import java.io.IOException; 41import java.io.InputStream; 42import java.net.URI; 43import java.net.URISyntaxException; 44import java.util.ArrayList; 45import java.util.HashMap; 46import java.util.HashSet; 47 48public class Pop3Store extends Store { 49 // All flags defining debug or development code settings must be FALSE 50 // when code is checked in or released. 51 private static boolean DEBUG_FORCE_SINGLE_LINE_UIDL = false; 52 private static boolean DEBUG_LOG_RAW_STREAM = false; 53 54 private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED }; 55 56 private Transport mTransport; 57 private String mUsername; 58 private String mPassword; 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 neccesary 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 neccesary 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(String uri, Context context, PersistentDataCallbacks callbacks) 90 throws MessagingException { 91 return new Pop3Store(uri); 92 } 93 94 /** 95 * pop3://user:password@server:port 96 * pop3+tls+://user:password@server:port 97 * pop3+tls+trustallcerts://user:password@server:port 98 * pop3+ssl+://user:password@server:port 99 * pop3+ssl+trustallcerts://user:password@server:port 100 * 101 * @param _uri 102 */ 103 private Pop3Store(String _uri) throws MessagingException { 104 URI uri; 105 try { 106 uri = new URI(_uri); 107 } catch (URISyntaxException use) { 108 throw new MessagingException("Invalid Pop3Store URI", use); 109 } 110 111 String scheme = uri.getScheme(); 112 if (scheme == null || !scheme.startsWith(STORE_SCHEME_POP3)) { 113 throw new MessagingException("Unsupported protocol"); 114 } 115 // defaults, which can be changed by security modifiers 116 int connectionSecurity = Transport.CONNECTION_SECURITY_NONE; 117 int defaultPort = 110; 118 // check for security modifiers and apply changes 119 if (scheme.contains("+ssl")) { 120 connectionSecurity = Transport.CONNECTION_SECURITY_SSL; 121 defaultPort = 995; 122 } else if (scheme.contains("+tls")) { 123 connectionSecurity = Transport.CONNECTION_SECURITY_TLS; 124 } 125 boolean trustCertificates = scheme.contains(STORE_SECURITY_TRUST_CERTIFICATES); 126 127 mTransport = new MailTransport("POP3"); 128 mTransport.setUri(uri, defaultPort); 129 mTransport.setSecurity(connectionSecurity, trustCertificates); 130 131 String[] userInfoParts = mTransport.getUserInfoParts(); 132 if (userInfoParts != null) { 133 mUsername = userInfoParts[0]; 134 if (userInfoParts.length > 1) { 135 mPassword = userInfoParts[1]; 136 } 137 } 138 } 139 140 /** 141 * For testing only. Injects a different transport. The transport should already be set 142 * up and ready to use. Do not use for real code. 143 * @param testTransport The Transport to inject and use for all future communication. 144 */ 145 /* package */ void setTransport(Transport testTransport) { 146 mTransport = testTransport; 147 } 148 149 @Override 150 public Folder getFolder(String name) throws MessagingException { 151 Folder folder = mFolders.get(name); 152 if (folder == null) { 153 folder = new Pop3Folder(name); 154 mFolders.put(folder.getName(), folder); 155 } 156 return folder; 157 } 158 159 @Override 160 public Folder[] getAllFolders() throws MessagingException { 161 return new Folder[] { 162 getFolder("INBOX"), 163 }; 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("INBOX"); 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, null); 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("INBOX")) { 202 mName = "INBOX"; 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, PersistentDataCallbacks callbacks) 244 throws MessagingException { 245 if (mTransport.isOpen()) { 246 return; 247 } 248 249 if (!mName.equalsIgnoreCase("INBOX")) { 250 throw new MessagingException("Folder does not exist"); 251 } 252 253 try { 254 mTransport.open(); 255 256 // Eat the banner 257 executeSimpleCommand(null); 258 259 mCapabilities = getCapabilities(); 260 261 if (mTransport.canTryTlsSecurity()) { 262 if (mCapabilities.stls) { 263 executeSimpleCommand("STLS"); 264 mTransport.reopenTls(); 265 } else { 266 if (Email.DEBUG) { 267 Log.d(Email.LOG_TAG, "TLS not supported but required"); 268 } 269 throw new MessagingException(MessagingException.TLS_REQUIRED); 270 } 271 } 272 273 try { 274 executeSensitiveCommand("USER " + mUsername, "USER /redacted/"); 275 executeSensitiveCommand("PASS " + mPassword, "PASS /redacted/"); 276 } catch (MessagingException me) { 277 if (Email.DEBUG) { 278 Log.d(Email.LOG_TAG, me.toString()); 279 } 280 throw new AuthenticationFailedException(null, me); 281 } 282 } catch (IOException ioe) { 283 mTransport.close(); 284 if (Email.DEBUG) { 285 Log.d(Email.LOG_TAG, ioe.toString()); 286 } 287 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 288 } 289 290 Exception statException = null; 291 try { 292 String response = executeSimpleCommand("STAT"); 293 String[] parts = response.split(" "); 294 if (parts.length < 2) { 295 statException = new IOException(); 296 } else { 297 mMessageCount = Integer.parseInt(parts[1]); 298 } 299 } catch (IOException ioe) { 300 statException = ioe; 301 } catch (NumberFormatException nfe) { 302 statException = nfe; 303 } 304 if (statException != null) { 305 mTransport.close(); 306 if (Email.DEBUG) { 307 Log.d(Email.LOG_TAG, statException.toString()); 308 } 309 throw new MessagingException("POP3 STAT", statException); 310 } 311 mUidToMsgMap.clear(); 312 mMsgNumToMsgMap.clear(); 313 mUidToMsgNumMap.clear(); 314 } 315 316 @Override 317 public OpenMode getMode() throws MessagingException { 318 return OpenMode.READ_WRITE; 319 } 320 321 /** 322 * Close the folder (and the transport below it). 323 * 324 * MUST NOT return any exceptions. 325 * 326 * @param expunge If true all deleted messages will be expunged (TODO - not implemented) 327 */ 328 @Override 329 public void close(boolean expunge) { 330 try { 331 executeSimpleCommand("QUIT"); 332 } 333 catch (Exception e) { 334 // ignore any problems here - just continue closing 335 } 336 mTransport.close(); 337 } 338 339 @Override 340 public String getName() { 341 return mName; 342 } 343 344 // POP3 does not folder creation 345 @Override 346 public boolean canCreate(FolderType type) { 347 return false; 348 } 349 350 @Override 351 public boolean create(FolderType type) throws MessagingException { 352 return false; 353 } 354 355 @Override 356 public boolean exists() throws MessagingException { 357 return mName.equalsIgnoreCase("INBOX"); 358 } 359 360 @Override 361 public int getMessageCount() { 362 return mMessageCount; 363 } 364 365 @Override 366 public int getUnreadMessageCount() throws MessagingException { 367 return -1; 368 } 369 370 @Override 371 public Message getMessage(String uid) throws MessagingException { 372 if (mUidToMsgNumMap.size() == 0) { 373 try { 374 indexMsgNums(1, mMessageCount); 375 } catch (IOException ioe) { 376 mTransport.close(); 377 if (Email.DEBUG) { 378 Log.d(Email.LOG_TAG, "Unable to index during getMessage " + ioe); 379 } 380 throw new MessagingException("getMessages", ioe); 381 } 382 } 383 Pop3Message message = mUidToMsgMap.get(uid); 384 return message; 385 } 386 387 @Override 388 public Message[] getMessages(int start, int end, MessageRetrievalListener listener) 389 throws MessagingException { 390 if (start < 1 || end < 1 || end < start) { 391 throw new MessagingException(String.format("Invalid message set %d %d", 392 start, end)); 393 } 394 try { 395 indexMsgNums(start, end); 396 } catch (IOException ioe) { 397 mTransport.close(); 398 if (Email.DEBUG) { 399 Log.d(Email.LOG_TAG, ioe.toString()); 400 } 401 throw new MessagingException("getMessages", ioe); 402 } 403 ArrayList<Message> messages = new ArrayList<Message>(); 404 for (int msgNum = start; msgNum <= end; msgNum++) { 405 Pop3Message message = mMsgNumToMsgMap.get(msgNum); 406 messages.add(message); 407 if (listener != null) { 408 listener.messageRetrieved(message); 409 } 410 } 411 return messages.toArray(new Message[messages.size()]); 412 } 413 414 /** 415 * Ensures that the given message set (from start to end inclusive) 416 * has been queried so that uids are available in the local cache. 417 * @param start 418 * @param end 419 * @throws MessagingException 420 * @throws IOException 421 */ 422 private void indexMsgNums(int start, int end) 423 throws MessagingException, IOException { 424 int unindexedMessageCount = 0; 425 for (int msgNum = start; msgNum <= end; msgNum++) { 426 if (mMsgNumToMsgMap.get(msgNum) == null) { 427 unindexedMessageCount++; 428 } 429 } 430 if (unindexedMessageCount == 0) { 431 return; 432 } 433 UidlParser parser = new UidlParser(); 434 if (DEBUG_FORCE_SINGLE_LINE_UIDL || 435 (unindexedMessageCount < 50 && mMessageCount > 5000)) { 436 /* 437 * In extreme cases we'll do a UIDL command per message instead of a bulk 438 * download. 439 */ 440 for (int msgNum = start; msgNum <= end; msgNum++) { 441 Pop3Message message = mMsgNumToMsgMap.get(msgNum); 442 if (message == null) { 443 String response = executeSimpleCommand("UIDL " + msgNum); 444 if (!parser.parseSingleLine(response)) { 445 throw new IOException(); 446 } 447 message = new Pop3Message(parser.mUniqueId, this); 448 indexMessage(msgNum, message); 449 } 450 } 451 } else { 452 String response = executeSimpleCommand("UIDL"); 453 while ((response = mTransport.readLine()) != null) { 454 if (!parser.parseMultiLine(response)) { 455 throw new IOException(); 456 } 457 if (parser.mEndOfMessage) { 458 break; 459 } 460 int msgNum = parser.mMessageNumber; 461 if (msgNum >= start && msgNum <= end) { 462 Pop3Message message = mMsgNumToMsgMap.get(msgNum); 463 if (message == null) { 464 message = new Pop3Message(parser.mUniqueId, this); 465 indexMessage(msgNum, message); 466 } 467 } 468 } 469 } 470 } 471 472 private void indexUids(ArrayList<String> uids) 473 throws MessagingException, IOException { 474 HashSet<String> unindexedUids = new HashSet<String>(); 475 for (String uid : uids) { 476 if (mUidToMsgMap.get(uid) == null) { 477 unindexedUids.add(uid); 478 } 479 } 480 if (unindexedUids.size() == 0) { 481 return; 482 } 483 /* 484 * If we are missing uids in the cache the only sure way to 485 * get them is to do a full UIDL list. A possible optimization 486 * would be trying UIDL for the latest X messages and praying. 487 */ 488 UidlParser parser = new UidlParser(); 489 String response = executeSimpleCommand("UIDL"); 490 while ((response = mTransport.readLine()) != null) { 491 parser.parseMultiLine(response); 492 if (parser.mEndOfMessage) { 493 break; 494 } 495 if (unindexedUids.contains(parser.mUniqueId)) { 496 Pop3Message message = mUidToMsgMap.get(parser.mUniqueId); 497 if (message == null) { 498 message = new Pop3Message(parser.mUniqueId, this); 499 } 500 indexMessage(parser.mMessageNumber, message); 501 } 502 } 503 } 504 505 /** 506 * Simple parser class for UIDL messages. 507 * 508 * <p>NOTE: In variance with RFC 1939, we allow multiple whitespace between the 509 * message-number and unique-id fields. This provides greater compatibility with some 510 * non-compliant POP3 servers, e.g. mail.comcast.net. 511 */ 512 /* package */ class UidlParser { 513 514 /** 515 * Caller can read back message-number from this field 516 */ 517 public int mMessageNumber; 518 /** 519 * Caller can read back unique-id from this field 520 */ 521 public String mUniqueId; 522 /** 523 * True if the response was "end-of-message" 524 */ 525 public boolean mEndOfMessage; 526 /** 527 * True if an error was reported 528 */ 529 public boolean mErr; 530 531 /** 532 * Construct & Initialize 533 */ 534 public UidlParser() { 535 mErr = true; 536 } 537 538 /** 539 * Parse a single-line response. This is returned from a command of the form 540 * "UIDL msg-num" and will be formatted as: "+OK msg-num unique-id" or 541 * "-ERR diagnostic text" 542 * 543 * @param response The string returned from the server 544 * @return true if the string parsed as expected (e.g. no syntax problems) 545 */ 546 public boolean parseSingleLine(String response) { 547 mErr = false; 548 if (response == null || response.length() == 0) { 549 return false; 550 } 551 char first = response.charAt(0); 552 if (first == '+') { 553 String[] uidParts = response.split(" +"); 554 if (uidParts.length >= 3) { 555 try { 556 mMessageNumber = Integer.parseInt(uidParts[1]); 557 } catch (NumberFormatException nfe) { 558 return false; 559 } 560 mUniqueId = uidParts[2]; 561 mEndOfMessage = true; 562 return true; 563 } 564 } else if (first == '-') { 565 mErr = true; 566 return true; 567 } 568 return false; 569 } 570 571 /** 572 * Parse a multi-line response. This is returned from a command of the form 573 * "UIDL" and will be formatted as: "." or "msg-num unique-id". 574 * 575 * @param response The string returned from the server 576 * @return true if the string parsed as expected (e.g. no syntax problems) 577 */ 578 public boolean parseMultiLine(String response) { 579 mErr = false; 580 if (response == null || response.length() == 0) { 581 return false; 582 } 583 char first = response.charAt(0); 584 if (first == '.') { 585 mEndOfMessage = true; 586 return true; 587 } else { 588 String[] uidParts = response.split(" +"); 589 if (uidParts.length >= 2) { 590 try { 591 mMessageNumber = Integer.parseInt(uidParts[0]); 592 } catch (NumberFormatException nfe) { 593 return false; 594 } 595 mUniqueId = uidParts[1]; 596 mEndOfMessage = false; 597 return true; 598 } 599 } 600 return false; 601 } 602 } 603 604 private void indexMessage(int msgNum, Pop3Message message) { 605 mMsgNumToMsgMap.put(msgNum, message); 606 mUidToMsgMap.put(message.getUid(), message); 607 mUidToMsgNumMap.put(message.getUid(), msgNum); 608 } 609 610 @Override 611 public Message[] getMessages(MessageRetrievalListener listener) throws MessagingException { 612 throw new UnsupportedOperationException("Pop3Folder.getMessage(MessageRetrievalListener)"); 613 } 614 615 @Override 616 public Message[] getMessages(String[] uids, MessageRetrievalListener listener) 617 throws MessagingException { 618 throw new UnsupportedOperationException("Pop3Folder.getMessage(MessageRetrievalListener)"); 619 } 620 621 /** 622 * Fetch the items contained in the FetchProfile into the given set of 623 * Messages in as efficient a manner as possible. 624 * @param messages 625 * @param fp 626 * @throws MessagingException 627 */ 628 @Override 629 public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener) 630 throws MessagingException { 631 if (messages == null || messages.length == 0) { 632 return; 633 } 634 ArrayList<String> uids = new ArrayList<String>(); 635 for (Message message : messages) { 636 uids.add(message.getUid()); 637 } 638 try { 639 indexUids(uids); 640 if (fp.contains(FetchProfile.Item.ENVELOPE)) { 641 // Note: We never pass the listener for the ENVELOPE call, because we're going 642 // to be calling the listener below in the per-message loop. 643 fetchEnvelope(messages, null); 644 } 645 } catch (IOException ioe) { 646 mTransport.close(); 647 if (Email.DEBUG) { 648 Log.d(Email.LOG_TAG, ioe.toString()); 649 } 650 throw new MessagingException("fetch", ioe); 651 } 652 for (int i = 0, count = messages.length; i < count; i++) { 653 Message message = messages[i]; 654 if (!(message instanceof Pop3Message)) { 655 throw new MessagingException("Pop3Store.fetch called with non-Pop3 Message"); 656 } 657 Pop3Message pop3Message = (Pop3Message)message; 658 try { 659 if (fp.contains(FetchProfile.Item.BODY)) { 660 fetchBody(pop3Message, -1); 661 } 662 else if (fp.contains(FetchProfile.Item.BODY_SANE)) { 663 /* 664 * To convert the suggested download size we take the size 665 * divided by the maximum line size (76). 666 */ 667 fetchBody(pop3Message, 668 FETCH_BODY_SANE_SUGGESTED_SIZE / 76); 669 } 670 else if (fp.contains(FetchProfile.Item.STRUCTURE)) { 671 /* 672 * If the user is requesting STRUCTURE we are required to set the body 673 * to null since we do not support the function. 674 */ 675 pop3Message.setBody(null); 676 } 677 if (listener != null) { 678 listener.messageRetrieved(message); 679 } 680 } catch (IOException ioe) { 681 mTransport.close(); 682 if (Email.DEBUG) { 683 Log.d(Email.LOG_TAG, ioe.toString()); 684 } 685 throw new MessagingException("Unable to fetch message", ioe); 686 } 687 } 688 } 689 690 private void fetchEnvelope(Message[] messages, 691 MessageRetrievalListener listener) throws IOException, MessagingException { 692 int unsizedMessages = 0; 693 for (Message message : messages) { 694 if (message.getSize() == -1) { 695 unsizedMessages++; 696 } 697 } 698 if (unsizedMessages == 0) { 699 return; 700 } 701 if (unsizedMessages < 50 && mMessageCount > 5000) { 702 /* 703 * In extreme cases we'll do a command per message instead of a bulk request 704 * to hopefully save some time and bandwidth. 705 */ 706 for (int i = 0, count = messages.length; i < count; i++) { 707 Message message = messages[i]; 708 if (!(message instanceof Pop3Message)) { 709 throw new MessagingException("Pop3Store.fetch called with non-Pop3 Message"); 710 } 711 Pop3Message pop3Message = (Pop3Message)message; 712 String response = executeSimpleCommand(String.format("LIST %d", 713 mUidToMsgNumMap.get(pop3Message.getUid()))); 714 try { 715 String[] listParts = response.split(" "); 716 int msgNum = Integer.parseInt(listParts[1]); 717 int msgSize = Integer.parseInt(listParts[2]); 718 pop3Message.setSize(msgSize); 719 } catch (NumberFormatException nfe) { 720 throw new IOException(); 721 } 722 if (listener != null) { 723 listener.messageRetrieved(pop3Message); 724 } 725 } 726 } else { 727 HashSet<String> msgUidIndex = new HashSet<String>(); 728 for (Message message : messages) { 729 msgUidIndex.add(message.getUid()); 730 } 731 String response = executeSimpleCommand("LIST"); 732 while ((response = mTransport.readLine()) != null) { 733 if (response.equals(".")) { 734 break; 735 } 736 Pop3Message pop3Message = null; 737 int msgSize = 0; 738 try { 739 String[] listParts = response.split(" "); 740 int msgNum = Integer.parseInt(listParts[0]); 741 msgSize = Integer.parseInt(listParts[1]); 742 pop3Message = mMsgNumToMsgMap.get(msgNum); 743 } catch (NumberFormatException nfe) { 744 throw new IOException(); 745 } 746 if (pop3Message != null && msgUidIndex.contains(pop3Message.getUid())) { 747 pop3Message.setSize(msgSize); 748 if (listener != null) { 749 listener.messageRetrieved(pop3Message); 750 } 751 } 752 } 753 } 754 } 755 756 /** 757 * Fetches the body of the given message, limiting the stored data 758 * to the specified number of lines. If lines is -1 the entire message 759 * is fetched. This is implemented with RETR for lines = -1 or TOP 760 * for any other value. If the server does not support TOP it is 761 * emulated with RETR and extra lines are thrown away. 762 * 763 * Note: Some servers (e.g. live.com) don't support CAPA, but turn out to 764 * support TOP after all. For better performance on these servers, we'll always 765 * probe TOP, and fall back to RETR when it's truly unsupported. 766 * 767 * @param message 768 * @param lines 769 */ 770 private void fetchBody(Pop3Message message, int lines) 771 throws IOException, MessagingException { 772 String response = null; 773 int messageId = mUidToMsgNumMap.get(message.getUid()); 774 if (lines == -1) { 775 // Fetch entire message 776 response = executeSimpleCommand(String.format("RETR %d", messageId)); 777 } else { 778 // Fetch partial message. Try "TOP", and fall back to slower "RETR" if necessary 779 try { 780 response = executeSimpleCommand(String.format("TOP %d %d", messageId, lines)); 781 } catch (MessagingException me) { 782 response = executeSimpleCommand(String.format("RETR %d", messageId)); 783 } 784 } 785 if (response != null) { 786 try { 787 InputStream in = mTransport.getInputStream(); 788 if (DEBUG_LOG_RAW_STREAM && Email.DEBUG) { 789 in = new LoggingInputStream(in); 790 } 791 message.parse(new Pop3ResponseInputStream(in)); 792 } 793 catch (MessagingException me) { 794 /* 795 * If we're only downloading headers it's possible 796 * we'll get a broken MIME message which we're not 797 * real worried about. If we've downloaded the body 798 * and can't parse it we need to let the user know. 799 */ 800 if (lines == -1) { 801 throw me; 802 } 803 } 804 } 805 } 806 807 @Override 808 public Flag[] getPermanentFlags() throws MessagingException { 809 return PERMANENT_FLAGS; 810 } 811 812 @Override 813 public void appendMessages(Message[] messages) throws MessagingException { 814 } 815 816 @Override 817 public void delete(boolean recurse) throws MessagingException { 818 } 819 820 @Override 821 public Message[] expunge() throws MessagingException { 822 return null; 823 } 824 825 @Override 826 public void setFlags(Message[] messages, Flag[] flags, boolean value) 827 throws MessagingException { 828 if (!value || !Utility.arrayContains(flags, Flag.DELETED)) { 829 /* 830 * The only flagging we support is setting the Deleted flag. 831 */ 832 return; 833 } 834 try { 835 for (Message message : messages) { 836 executeSimpleCommand(String.format("DELE %s", 837 mUidToMsgNumMap.get(message.getUid()))); 838 } 839 } 840 catch (IOException ioe) { 841 mTransport.close(); 842 if (Email.DEBUG) { 843 Log.d(Email.LOG_TAG, ioe.toString()); 844 } 845 throw new MessagingException("setFlags()", ioe); 846 } 847 } 848 849 @Override 850 public void copyMessages(Message[] msgs, Folder folder, MessageUpdateCallbacks callbacks) 851 throws MessagingException { 852 throw new UnsupportedOperationException("copyMessages is not supported in POP3"); 853 } 854 855// private boolean isRoundTripModeSuggested() { 856// long roundTripMethodMs = 857// (uncachedMessageCount * 2 * mLatencyMs); 858// long bulkMethodMs = 859// (mMessageCount * 58) / (mThroughputKbS * 1024 / 8) * 1000; 860// } 861 862 private Pop3Capabilities getCapabilities() throws IOException, MessagingException { 863 Pop3Capabilities capabilities = new Pop3Capabilities(); 864 try { 865 String response = executeSimpleCommand("CAPA"); 866 while ((response = mTransport.readLine()) != null) { 867 if (response.equals(".")) { 868 break; 869 } 870 if (response.equalsIgnoreCase("STLS")){ 871 capabilities.stls = true; 872 } 873 else if (response.equalsIgnoreCase("UIDL")) { 874 capabilities.uidl = true; 875 } 876 else if (response.equalsIgnoreCase("PIPELINING")) { 877 capabilities.pipelining = true; 878 } 879 else if (response.equalsIgnoreCase("USER")) { 880 capabilities.user = true; 881 } 882 else if (response.equalsIgnoreCase("TOP")) { 883 capabilities.top = true; 884 } 885 } 886 } 887 catch (MessagingException me) { 888 /* 889 * The server may not support the CAPA command, so we just eat this Exception 890 * and allow the empty capabilities object to be returned. 891 */ 892 } 893 return capabilities; 894 } 895 896 /** 897 * Send a single command and wait for a single line response. Reopens the connection, 898 * if it is closed. Leaves the connection open. 899 * 900 * @param command The command string to send to the server. 901 * @return Returns the response string from the server. 902 */ 903 private String executeSimpleCommand(String command) throws IOException, MessagingException { 904 return executeSensitiveCommand(command, null); 905 } 906 907 /** 908 * Send a single command and wait for a single line response. Reopens the connection, 909 * if it is closed. Leaves the connection open. 910 * 911 * @param command The command string to send to the server. 912 * @param sensitiveReplacement If the command includes sensitive data (e.g. authentication) 913 * please pass a replacement string here (for logging). 914 * @return Returns the response string from the server. 915 */ 916 private String executeSensitiveCommand(String command, String sensitiveReplacement) 917 throws IOException, MessagingException { 918 open(OpenMode.READ_WRITE, null); 919 920 if (command != null) { 921 mTransport.writeLine(command, sensitiveReplacement); 922 } 923 924 String response = mTransport.readLine(); 925 926 if (response.length() > 1 && response.charAt(0) == '-') { 927 throw new MessagingException(response); 928 } 929 930 return response; 931 } 932 933 @Override 934 public boolean equals(Object o) { 935 if (o instanceof Pop3Folder) { 936 return ((Pop3Folder) o).mName.equals(mName); 937 } 938 return super.equals(o); 939 } 940 941 @Override 942 // TODO this is deprecated, eventually discard 943 public boolean isOpen() { 944 return mTransport.isOpen(); 945 } 946 947 @Override 948 public Message createMessage(String uid) throws MessagingException { 949 return new Pop3Message(uid, this); 950 } 951 } 952 953 public static class Pop3Message extends MimeMessage { 954 public Pop3Message(String uid, Pop3Folder folder) throws MessagingException { 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