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