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