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