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