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