Pop3Store.java revision 7f7f7e6402eec1baab6bedcb58da61369cae4097
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; 50 51public class Pop3Store extends Store { 52 // All flags defining debug or development code settings must be FALSE 53 // when code is checked in or released. 54 private static boolean DEBUG_FORCE_SINGLE_LINE_UIDL = false; 55 private static boolean DEBUG_LOG_RAW_STREAM = false; 56 57 private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED }; 58 /** The name of the only mailbox available to POP3 accounts */ 59 private static final String POP3_MAILBOX_NAME = "INBOX"; 60 private final HashMap<String, Folder> mFolders = new HashMap<String, Folder>(); 61 private final Message[] mOneMessage = new Message[1]; 62 63 /** 64 * Static named constructor. 65 */ 66 public static Store newInstance(Account account, Context context) throws MessagingException { 67 return new Pop3Store(context, account); 68 } 69 70 /** 71 * Creates a new store for the given account. 72 */ 73 private Pop3Store(Context context, Account account) throws MessagingException { 74 mContext = context; 75 mAccount = account; 76 77 HostAuth recvAuth = account.getOrCreateHostAuthRecv(context); 78 if (recvAuth == null || !HostAuth.LEGACY_SCHEME_POP3.equalsIgnoreCase(recvAuth.mProtocol)) { 79 throw new MessagingException("Unsupported protocol"); 80 } 81 mTransport = new MailTransport(context, "POP3", recvAuth); 82 String[] userInfoParts = recvAuth.getLogin(); 83 if (userInfoParts != null) { 84 mUsername = userInfoParts[0]; 85 mPassword = userInfoParts[1]; 86 } 87 } 88 89 /** 90 * For testing only. Injects a different transport. The transport should already be set 91 * up and ready to use. Do not use for real code. 92 * @param testTransport The Transport to inject and use for all future communication. 93 */ 94 /* package */ void setTransport(Transport testTransport) { 95 mTransport = testTransport; 96 } 97 98 @Override 99 public Folder getFolder(String name) { 100 Folder folder = mFolders.get(name); 101 if (folder == null) { 102 folder = new Pop3Folder(name); 103 mFolders.put(folder.getName(), folder); 104 } 105 return folder; 106 } 107 108 private final int[] DEFAULT_FOLDERS = { 109 Mailbox.TYPE_DRAFTS, 110 Mailbox.TYPE_OUTBOX, 111 Mailbox.TYPE_SENT, 112 Mailbox.TYPE_TRASH 113 }; 114 115 @Override 116 public Folder[] updateFolders() { 117 String inboxName = mContext.getString(R.string.mailbox_name_display_inbox); 118 Mailbox mailbox = Mailbox.getMailboxForPath(mContext, mAccount.mId, inboxName); 119 updateMailbox(mailbox, mAccount.mId, inboxName, '\0', true, Mailbox.TYPE_INBOX); 120 // Force the parent key to be "no mailbox" for the mail POP3 mailbox 121 mailbox.mParentKey = Mailbox.NO_MAILBOX; 122 if (mailbox.isSaved()) { 123 mailbox.update(mContext, mailbox.toContentValues()); 124 } else { 125 mailbox.save(mContext); 126 } 127 128 // Build default mailboxes as well, in case they're not already made. 129 for (int type : DEFAULT_FOLDERS) { 130 if (Mailbox.findMailboxOfType(mContext, mAccount.mId, type) == Mailbox.NO_MAILBOX) { 131 String name = getMailboxServerName(mContext, type); 132 mailbox = Mailbox.newSystemMailbox(mAccount.mId, type, name); 133 mailbox.save(mContext); 134 } 135 } 136 137 return new Folder[] { getFolder(inboxName) }; 138 } 139 140 141 /** 142 * Returns the server-side name for a specific mailbox. 143 * 144 * @return the resource string corresponding to the mailbox type, empty if not found. 145 */ 146 public String getMailboxServerName(Context context, int mailboxType) { 147 int resId = -1; 148 switch (mailboxType) { 149 case Mailbox.TYPE_INBOX: 150 resId = R.string.mailbox_name_server_inbox; 151 break; 152 case Mailbox.TYPE_OUTBOX: 153 resId = R.string.mailbox_name_server_outbox; 154 break; 155 case Mailbox.TYPE_DRAFTS: 156 resId = R.string.mailbox_name_server_drafts; 157 break; 158 case Mailbox.TYPE_TRASH: 159 resId = R.string.mailbox_name_server_trash; 160 break; 161 case Mailbox.TYPE_SENT: 162 resId = R.string.mailbox_name_server_sent; 163 break; 164 case Mailbox.TYPE_JUNK: 165 resId = R.string.mailbox_name_server_junk; 166 break; 167 } 168 return resId != -1 ? context.getString(resId) : ""; 169 } 170 171 /** 172 * Used by account setup to test if an account's settings are appropriate. The definition 173 * of "checked" here is simply, can you log into the account and does it meet some minimum set 174 * of feature requirements? 175 * 176 * @throws MessagingException if there was some problem with the account 177 */ 178 @Override 179 public Bundle checkSettings() throws MessagingException { 180 Pop3Folder folder = new Pop3Folder(POP3_MAILBOX_NAME); 181 Bundle bundle = null; 182 // Close any open or half-open connections - checkSettings should always be "fresh" 183 if (mTransport.isOpen()) { 184 folder.close(false); 185 } 186 try { 187 folder.open(OpenMode.READ_WRITE); 188 bundle = folder.checkSettings(); 189 } finally { 190 folder.close(false); // false == don't expunge anything 191 } 192 return bundle; 193 } 194 195 public class Pop3Folder extends Folder { 196 private final HashMap<String, Pop3Message> mUidToMsgMap 197 = new HashMap<String, Pop3Message>(); 198 private final HashMap<Integer, Pop3Message> mMsgNumToMsgMap 199 = new HashMap<Integer, Pop3Message>(); 200 private final HashMap<String, Integer> mUidToMsgNumMap = new HashMap<String, Integer>(); 201 private final String mName; 202 private int mMessageCount; 203 private Pop3Capabilities mCapabilities; 204 205 public Pop3Folder(String name) { 206 if (name.equalsIgnoreCase(POP3_MAILBOX_NAME)) { 207 mName = POP3_MAILBOX_NAME; 208 } else { 209 mName = name; 210 } 211 } 212 213 /** 214 * Used by account setup to test if an account's settings are appropriate. Here, we run 215 * an additional test to see if UIDL is supported on the server. If it's not we 216 * can't service this account. 217 * 218 * @return Bundle containing validation data (code and, if appropriate, error message) 219 * @throws MessagingException if the account is not going to be useable 220 */ 221 public Bundle checkSettings() throws MessagingException { 222 Bundle bundle = new Bundle(); 223 int result = MessagingException.NO_ERROR; 224 try { 225 UidlParser parser = new UidlParser(); 226 executeSimpleCommand("UIDL"); 227 // drain the entire output, so additional communications don't get confused. 228 String response; 229 while ((response = mTransport.readLine()) != null) { 230 parser.parseMultiLine(response); 231 if (parser.mEndOfMessage) { 232 break; 233 } 234 } 235 } catch (IOException ioe) { 236 mTransport.close(); 237 result = MessagingException.IOERROR; 238 bundle.putString(EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE, 239 ioe.getMessage()); 240 } 241 bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, result); 242 return bundle; 243 } 244 245 @Override 246 public synchronized void open(OpenMode mode) throws MessagingException { 247 if (mTransport.isOpen()) { 248 return; 249 } 250 251 if (!mName.equalsIgnoreCase(POP3_MAILBOX_NAME)) { 252 throw new MessagingException("Folder does not exist"); 253 } 254 255 try { 256 mTransport.open(); 257 258 // Eat the banner 259 executeSimpleCommand(null); 260 261 mCapabilities = getCapabilities(); 262 263 if (mTransport.canTryTlsSecurity()) { 264 if (mCapabilities.stls) { 265 executeSimpleCommand("STLS"); 266 mTransport.reopenTls(); 267 } else { 268 if (MailActivityEmail.DEBUG) { 269 Log.d(Logging.LOG_TAG, "TLS not supported but required"); 270 } 271 throw new MessagingException(MessagingException.TLS_REQUIRED); 272 } 273 } 274 275 try { 276 executeSensitiveCommand("USER " + mUsername, "USER /redacted/"); 277 executeSensitiveCommand("PASS " + mPassword, "PASS /redacted/"); 278 } catch (MessagingException me) { 279 if (MailActivityEmail.DEBUG) { 280 Log.d(Logging.LOG_TAG, me.toString()); 281 } 282 throw new AuthenticationFailedException(null, me); 283 } 284 } catch (IOException ioe) { 285 mTransport.close(); 286 if (MailActivityEmail.DEBUG) { 287 Log.d(Logging.LOG_TAG, ioe.toString()); 288 } 289 throw new MessagingException(MessagingException.IOERROR, ioe.toString()); 290 } 291 292 Exception statException = null; 293 try { 294 String response = executeSimpleCommand("STAT"); 295 String[] parts = response.split(" "); 296 if (parts.length < 2) { 297 statException = new IOException(); 298 } else { 299 mMessageCount = Integer.parseInt(parts[1]); 300 } 301 } catch (IOException ioe) { 302 statException = ioe; 303 } catch (NumberFormatException nfe) { 304 statException = nfe; 305 } 306 if (statException != null) { 307 mTransport.close(); 308 if (MailActivityEmail.DEBUG) { 309 Log.d(Logging.LOG_TAG, statException.toString()); 310 } 311 throw new MessagingException("POP3 STAT", statException); 312 } 313 mUidToMsgMap.clear(); 314 mMsgNumToMsgMap.clear(); 315 mUidToMsgNumMap.clear(); 316 } 317 318 @Override 319 public OpenMode getMode() { 320 return OpenMode.READ_WRITE; 321 } 322 323 /** 324 * Close the folder (and the transport below it). 325 * 326 * MUST NOT return any exceptions. 327 * 328 * @param expunge If true all deleted messages will be expunged (TODO - not implemented) 329 */ 330 @Override 331 public void close(boolean expunge) { 332 try { 333 executeSimpleCommand("QUIT"); 334 } 335 catch (Exception e) { 336 // ignore any problems here - just continue closing 337 } 338 mTransport.close(); 339 } 340 341 @Override 342 public String getName() { 343 return mName; 344 } 345 346 // POP3 does not folder creation 347 @Override 348 public boolean canCreate(FolderType type) { 349 return false; 350 } 351 352 @Override 353 public boolean create(FolderType type) { 354 return false; 355 } 356 357 @Override 358 public boolean exists() { 359 return mName.equalsIgnoreCase(POP3_MAILBOX_NAME); 360 } 361 362 @Override 363 public int getMessageCount() { 364 return mMessageCount; 365 } 366 367 @Override 368 public int getUnreadMessageCount() { 369 return -1; 370 } 371 372 @Override 373 public Message getMessage(String uid) throws MessagingException { 374 if (mUidToMsgNumMap.size() == 0) { 375 try { 376 indexMsgNums(1, mMessageCount); 377 } catch (IOException ioe) { 378 mTransport.close(); 379 if (MailActivityEmail.DEBUG) { 380 Log.d(Logging.LOG_TAG, "Unable to index during getMessage " + ioe); 381 } 382 throw new MessagingException("getMessages", ioe); 383 } 384 } 385 Pop3Message message = mUidToMsgMap.get(uid); 386 return message; 387 } 388 389 @Override 390 public Pop3Message[] getMessages(int start, int end, MessageRetrievalListener listener) 391 throws MessagingException { 392 return null; 393 } 394 395 public Pop3Message[] getMessages(int end, final int limit) 396 throws MessagingException { 397 try { 398 indexMsgNums(1, end); 399 } catch (IOException ioe) { 400 mTransport.close(); 401 if (MailActivityEmail.DEBUG) { 402 Log.d(Logging.LOG_TAG, ioe.toString()); 403 } 404 throw new MessagingException("getMessages", ioe); 405 } 406 ArrayList<Message> messages = new ArrayList<Message>(); 407 for (int msgNum = end; msgNum > 0 && (messages.size() < limit); msgNum--) { 408 Pop3Message message = mMsgNumToMsgMap.get(msgNum); 409 if (message != null) { 410 messages.add(message); 411 } 412 } 413 return messages.toArray(new Pop3Message[messages.size()]); 414 } 415 416 /** 417 * Ensures that the given message set (from start to end inclusive) 418 * has been queried so that uids are available in the local cache. 419 * @param start 420 * @param end 421 * @throws MessagingException 422 * @throws IOException 423 */ 424 private void indexMsgNums(int start, int end) 425 throws MessagingException, IOException { 426 if (!mMsgNumToMsgMap.isEmpty()) { 427 return; 428 } 429 UidlParser parser = new UidlParser(); 430 if (DEBUG_FORCE_SINGLE_LINE_UIDL || (mMessageCount > 5000)) { 431 /* 432 * In extreme cases we'll do a UIDL command per message instead of a bulk 433 * download. 434 */ 435 for (int msgNum = start; msgNum <= end; msgNum++) { 436 Pop3Message message = mMsgNumToMsgMap.get(msgNum); 437 if (message == null) { 438 String response = executeSimpleCommand("UIDL " + msgNum); 439 if (!parser.parseSingleLine(response)) { 440 throw new IOException(); 441 } 442 message = new Pop3Message(parser.mUniqueId, this); 443 indexMessage(msgNum, message); 444 } 445 } 446 } else { 447 String response = executeSimpleCommand("UIDL"); 448 while ((response = mTransport.readLine()) != null) { 449 if (!parser.parseMultiLine(response)) { 450 throw new IOException(); 451 } 452 if (parser.mEndOfMessage) { 453 break; 454 } 455 int msgNum = parser.mMessageNumber; 456 if (msgNum >= start && msgNum <= end) { 457 Pop3Message message = mMsgNumToMsgMap.get(msgNum); 458 if (message == null) { 459 message = new Pop3Message(parser.mUniqueId, this); 460 indexMessage(msgNum, message); 461 } 462 } 463 } 464 } 465 } 466 467 /** 468 * Simple parser class for UIDL messages. 469 * 470 * <p>NOTE: In variance with RFC 1939, we allow multiple whitespace between the 471 * message-number and unique-id fields. This provides greater compatibility with some 472 * non-compliant POP3 servers, e.g. mail.comcast.net. 473 */ 474 /* package */ class UidlParser { 475 476 /** 477 * Caller can read back message-number from this field 478 */ 479 public int mMessageNumber; 480 /** 481 * Caller can read back unique-id from this field 482 */ 483 public String mUniqueId; 484 /** 485 * True if the response was "end-of-message" 486 */ 487 public boolean mEndOfMessage; 488 /** 489 * True if an error was reported 490 */ 491 public boolean mErr; 492 493 /** 494 * Construct & Initialize 495 */ 496 public UidlParser() { 497 mErr = true; 498 } 499 500 /** 501 * Parse a single-line response. This is returned from a command of the form 502 * "UIDL msg-num" and will be formatted as: "+OK msg-num unique-id" or 503 * "-ERR diagnostic text" 504 * 505 * @param response The string returned from the server 506 * @return true if the string parsed as expected (e.g. no syntax problems) 507 */ 508 public boolean parseSingleLine(String response) { 509 mErr = false; 510 if (response == null || response.length() == 0) { 511 return false; 512 } 513 char first = response.charAt(0); 514 if (first == '+') { 515 String[] uidParts = response.split(" +"); 516 if (uidParts.length >= 3) { 517 try { 518 mMessageNumber = Integer.parseInt(uidParts[1]); 519 } catch (NumberFormatException nfe) { 520 return false; 521 } 522 mUniqueId = uidParts[2]; 523 mEndOfMessage = true; 524 return true; 525 } 526 } else if (first == '-') { 527 mErr = true; 528 return true; 529 } 530 return false; 531 } 532 533 /** 534 * Parse a multi-line response. This is returned from a command of the form 535 * "UIDL" and will be formatted as: "." or "msg-num unique-id". 536 * 537 * @param response The string returned from the server 538 * @return true if the string parsed as expected (e.g. no syntax problems) 539 */ 540 public boolean parseMultiLine(String response) { 541 mErr = false; 542 if (response == null || response.length() == 0) { 543 return false; 544 } 545 char first = response.charAt(0); 546 if (first == '.') { 547 mEndOfMessage = true; 548 return true; 549 } else { 550 String[] uidParts = response.split(" +"); 551 if (uidParts.length >= 2) { 552 try { 553 mMessageNumber = Integer.parseInt(uidParts[0]); 554 } catch (NumberFormatException nfe) { 555 return false; 556 } 557 mUniqueId = uidParts[1]; 558 mEndOfMessage = false; 559 return true; 560 } 561 } 562 return false; 563 } 564 } 565 566 private void indexMessage(int msgNum, Pop3Message message) { 567 mMsgNumToMsgMap.put(msgNum, message); 568 mUidToMsgMap.put(message.getUid(), message); 569 mUidToMsgNumMap.put(message.getUid(), msgNum); 570 } 571 572 @Override 573 public Message[] getMessages(String[] uids, MessageRetrievalListener listener) { 574 throw new UnsupportedOperationException( 575 "Pop3Folder.getMessage(MessageRetrievalListener)"); 576 } 577 578 /** 579 * Fetch the items contained in the FetchProfile into the given set of 580 * Messages in as efficient a manner as possible. 581 * @param messages 582 * @param fp 583 * @throws MessagingException 584 */ 585 @Override 586 public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener) 587 throws MessagingException { 588 throw new UnsupportedOperationException( 589 "Pop3Folder.fetch(Message[], FetchProfile, MessageRetrievalListener)"); 590 } 591 592 /** 593 * Fetches the body of the given message, limiting the stored data 594 * to the specified number of lines. If lines is -1 the entire message 595 * is fetched. This is implemented with RETR for lines = -1 or TOP 596 * for any other value. If the server does not support TOP it is 597 * emulated with RETR and extra lines are thrown away. 598 * 599 * Note: Some servers (e.g. live.com) don't support CAPA, but turn out to 600 * support TOP after all. For better performance on these servers, we'll always 601 * probe TOP, and fall back to RETR when it's truly unsupported. 602 * 603 * @param message 604 * @param lines 605 */ 606 public void fetchBody(Pop3Message message, int lines) 607 throws IOException, MessagingException { 608 String response = null; 609 int messageId = mUidToMsgNumMap.get(message.getUid()); 610 if (lines == -1) { 611 // Fetch entire message 612 response = executeSimpleCommand(String.format("RETR %d", messageId)); 613 } else { 614 // Fetch partial message. Try "TOP", and fall back to slower "RETR" if necessary 615 try { 616 response = executeSimpleCommand(String.format("TOP %d %d", messageId, lines)); 617 } catch (MessagingException me) { 618 try { 619 response = executeSimpleCommand(String.format("RETR %d", messageId)); 620 } catch (MessagingException e) { 621 Log.w(Logging.LOG_TAG, "Can't read message " + messageId); 622 } 623 } 624 } 625 if (response != null) { 626 try { 627 int ok = response.indexOf("OK"); 628 if (ok > 0) { 629 try { 630 int start = ok + 3; 631 int end = response.indexOf(" ", start); 632 String intString; 633 if (end > 0) { 634 intString = response.substring(start, end); 635 } else { 636 intString = response.substring(start); 637 } 638 message.setSize(Integer.parseInt(intString)); 639 } catch (NumberFormatException e) { 640 // We tried 641 } 642 } 643 InputStream in = mTransport.getInputStream(); 644 if (DEBUG_LOG_RAW_STREAM && MailActivityEmail.DEBUG) { 645 in = new LoggingInputStream(in); 646 } 647 message.parse(new Pop3ResponseInputStream(in)); 648 } 649 catch (MessagingException me) { 650 /* 651 * If we're only downloading headers it's possible 652 * we'll get a broken MIME message which we're not 653 * real worried about. If we've downloaded the body 654 * and can't parse it we need to let the user know. 655 */ 656 if (lines == -1) { 657 throw me; 658 } 659 } 660 } 661 } 662 663 @Override 664 public Flag[] getPermanentFlags() { 665 return PERMANENT_FLAGS; 666 } 667 668 @Override 669 public void appendMessages(Message[] messages) { 670 } 671 672 @Override 673 public void delete(boolean recurse) { 674 } 675 676 @Override 677 public Message[] expunge() { 678 return null; 679 } 680 681 public void deleteMessage(Message message) throws MessagingException { 682 mOneMessage[0] = message; 683 setFlags(mOneMessage, PERMANENT_FLAGS, true); 684 } 685 686 @Override 687 public void setFlags(Message[] messages, Flag[] flags, boolean value) 688 throws MessagingException { 689 if (!value || !Utility.arrayContains(flags, Flag.DELETED)) { 690 /* 691 * The only flagging we support is setting the Deleted flag. 692 */ 693 return; 694 } 695 try { 696 for (Message message : messages) { 697 try { 698 String uid = message.getUid(); 699 int msgNum = mUidToMsgNumMap.get(uid); 700 executeSimpleCommand(String.format("DELE %s", msgNum)); 701 // Remove from the maps 702 mMsgNumToMsgMap.remove(msgNum); 703 mUidToMsgNumMap.remove(uid); 704 } catch (MessagingException e) { 705 // A failed deletion isn't a problem 706 } 707 } 708 } 709 catch (IOException ioe) { 710 mTransport.close(); 711 if (MailActivityEmail.DEBUG) { 712 Log.d(Logging.LOG_TAG, ioe.toString()); 713 } 714 throw new MessagingException("setFlags()", ioe); 715 } 716 } 717 718 @Override 719 public void copyMessages(Message[] msgs, Folder folder, MessageUpdateCallbacks callbacks) { 720 throw new UnsupportedOperationException("copyMessages is not supported in POP3"); 721 } 722 723 private Pop3Capabilities getCapabilities() throws IOException { 724 Pop3Capabilities capabilities = new Pop3Capabilities(); 725 try { 726 String response = executeSimpleCommand("CAPA"); 727 while ((response = mTransport.readLine()) != null) { 728 if (response.equals(".")) { 729 break; 730 } else if (response.equalsIgnoreCase("STLS")){ 731 capabilities.stls = true; 732 } 733 } 734 } 735 catch (MessagingException me) { 736 /* 737 * The server may not support the CAPA command, so we just eat this Exception 738 * and allow the empty capabilities object to be returned. 739 */ 740 } 741 return capabilities; 742 } 743 744 /** 745 * Send a single command and wait for a single line response. Reopens the connection, 746 * if it is closed. Leaves the connection open. 747 * 748 * @param command The command string to send to the server. 749 * @return Returns the response string from the server. 750 */ 751 private String executeSimpleCommand(String command) throws IOException, MessagingException { 752 return executeSensitiveCommand(command, null); 753 } 754 755 /** 756 * Send a single command and wait for a single line response. Reopens the connection, 757 * if it is closed. Leaves the connection open. 758 * 759 * @param command The command string to send to the server. 760 * @param sensitiveReplacement If the command includes sensitive data (e.g. authentication) 761 * please pass a replacement string here (for logging). 762 * @return Returns the response string from the server. 763 */ 764 private String executeSensitiveCommand(String command, String sensitiveReplacement) 765 throws IOException, MessagingException { 766 open(OpenMode.READ_WRITE); 767 768 if (command != null) { 769 mTransport.writeLine(command, sensitiveReplacement); 770 } 771 772 String response = mTransport.readLine(); 773 774 if (response.length() > 1 && response.charAt(0) == '-') { 775 throw new MessagingException(response); 776 } 777 778 return response; 779 } 780 781 @Override 782 public boolean equals(Object o) { 783 if (o instanceof Pop3Folder) { 784 return ((Pop3Folder) o).mName.equals(mName); 785 } 786 return super.equals(o); 787 } 788 789 @Override 790 @VisibleForTesting 791 public boolean isOpen() { 792 return mTransport.isOpen(); 793 } 794 795 @Override 796 public Message createMessage(String uid) { 797 return new Pop3Message(uid, this); 798 } 799 800 @Override 801 public Message[] getMessages(SearchParams params, MessageRetrievalListener listener) { 802 return null; 803 } 804 } 805 806 public static class Pop3Message extends MimeMessage { 807 public Pop3Message(String uid, Pop3Folder folder) { 808 mUid = uid; 809 mFolder = folder; 810 mSize = -1; 811 } 812 813 public void setSize(int size) { 814 mSize = size; 815 } 816 817 @Override 818 public void parse(InputStream in) throws IOException, MessagingException { 819 super.parse(in); 820 } 821 822 @Override 823 public void setFlag(Flag flag, boolean set) throws MessagingException { 824 super.setFlag(flag, set); 825 mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set); 826 } 827 } 828 829 /** 830 * POP3 Capabilities as defined in RFC 2449. This is not a complete list of CAPA 831 * responses - just those that we use in this client. 832 */ 833 class Pop3Capabilities { 834 /** The STLS (start TLS) command is supported */ 835 public boolean stls; 836 837 @Override 838 public String toString() { 839 return String.format("STLS %b", stls); 840 } 841 } 842 843 // TODO figure out what is special about this and merge it into MailTransport 844 class Pop3ResponseInputStream extends InputStream { 845 private final InputStream mIn; 846 private boolean mStartOfLine = true; 847 private boolean mFinished; 848 849 public Pop3ResponseInputStream(InputStream in) { 850 mIn = in; 851 } 852 853 @Override 854 public int read() throws IOException { 855 if (mFinished) { 856 return -1; 857 } 858 int d = mIn.read(); 859 if (mStartOfLine && d == '.') { 860 d = mIn.read(); 861 if (d == '\r') { 862 mFinished = true; 863 mIn.read(); 864 return -1; 865 } 866 } 867 868 mStartOfLine = (d == '\n'); 869 870 return d; 871 } 872 } 873} 874