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