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