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