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