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