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