replicant-packages_apps_Email/src/com/android/email/mail/store/Pop3Store.java

1032 lines
39 KiB
Java

/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.email.mail.store;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import com.android.email.Email;
import com.android.email.mail.Store;
import com.android.email.mail.Transport;
import com.android.email.mail.transport.MailTransport;
import com.android.emailcommon.Logging;
import com.android.emailcommon.internet.MimeMessage;
import com.android.emailcommon.mail.AuthenticationFailedException;
import com.android.emailcommon.mail.FetchProfile;
import com.android.emailcommon.mail.Flag;
import com.android.emailcommon.mail.Folder;
import com.android.emailcommon.mail.Folder.OpenMode;
import com.android.emailcommon.mail.Message;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.service.EmailServiceProxy;
import com.android.emailcommon.service.SearchParams;
import com.android.emailcommon.utility.LoggingInputStream;
import com.android.emailcommon.utility.Utility;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
public class Pop3Store extends Store {
// All flags defining debug or development code settings must be FALSE
// when code is checked in or released.
private static boolean DEBUG_FORCE_SINGLE_LINE_UIDL = false;
private static boolean DEBUG_LOG_RAW_STREAM = false;
private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED };
/** The name of the only mailbox available to POP3 accounts */
private static final String POP3_MAILBOX_NAME = "INBOX";
private final HashMap<String, Folder> mFolders = new HashMap<String, Folder>();
// /**
// * Detected latency, used for usage scaling.
// * Usage scaling occurs when it is necessary to get information about
// * messages that could result in large data loads. This value allows
// * the code that loads this data to decide between using large downloads
// * (high latency) or multiple round trips (low latency) to accomplish
// * the same thing.
// * Default is Integer.MAX_VALUE implying massive latency so that the large
// * download method is used by default until latency data is collected.
// */
// private int mLatencyMs = Integer.MAX_VALUE;
//
// /**
// * Detected throughput, used for usage scaling.
// * Usage scaling occurs when it is necessary to get information about
// * messages that could result in large data loads. This value allows
// * the code that loads this data to decide between using large downloads
// * (high latency) or multiple round trips (low latency) to accomplish
// * the same thing.
// * Default is Integer.MAX_VALUE implying massive bandwidth so that the
// * large download method is used by default until latency data is
// * collected.
// */
// private int mThroughputKbS = Integer.MAX_VALUE;
/**
* Static named constructor.
*/
public static Store newInstance(Account account, Context context) throws MessagingException {
return new Pop3Store(context, account);
}
/**
* Creates a new store for the given account.
*/
private Pop3Store(Context context, Account account) throws MessagingException {
mContext = context;
mAccount = account;
HostAuth recvAuth = account.getOrCreateHostAuthRecv(context);
if (recvAuth == null || !STORE_SCHEME_POP3.equalsIgnoreCase(recvAuth.mProtocol)) {
throw new MessagingException("Unsupported protocol");
}
// defaults, which can be changed by security modifiers
int connectionSecurity = Transport.CONNECTION_SECURITY_NONE;
int defaultPort = 110;
// check for security flags and apply changes
if ((recvAuth.mFlags & HostAuth.FLAG_SSL) != 0) {
connectionSecurity = Transport.CONNECTION_SECURITY_SSL;
defaultPort = 995;
} else if ((recvAuth.mFlags & HostAuth.FLAG_TLS) != 0) {
connectionSecurity = Transport.CONNECTION_SECURITY_TLS;
}
boolean trustCertificates = ((recvAuth.mFlags & HostAuth.FLAG_TRUST_ALL) != 0);
int port = defaultPort;
if (recvAuth.mPort != HostAuth.PORT_UNKNOWN) {
port = recvAuth.mPort;
}
mTransport = new MailTransport("POP3");
mTransport.setHost(recvAuth.mAddress);
mTransport.setPort(port);
mTransport.setSecurity(connectionSecurity, trustCertificates);
String[] userInfoParts = recvAuth.getLogin();
if (userInfoParts != null) {
mUsername = userInfoParts[0];
mPassword = userInfoParts[1];
}
}
/**
* For testing only. Injects a different transport. The transport should already be set
* up and ready to use. Do not use for real code.
* @param testTransport The Transport to inject and use for all future communication.
*/
/* package */ void setTransport(Transport testTransport) {
mTransport = testTransport;
}
@Override
public Folder getFolder(String name) {
Folder folder = mFolders.get(name);
if (folder == null) {
folder = new Pop3Folder(name);
mFolders.put(folder.getName(), folder);
}
return folder;
}
@Override
public Folder[] updateFolders() {
Mailbox mailbox = getMailboxForPath(mContext, mAccount.mId, POP3_MAILBOX_NAME);
updateMailbox(mailbox, mAccount.mId, POP3_MAILBOX_NAME, '\0', true, Mailbox.TYPE_INBOX);
// Force the parent key to be "no mailbox" for the mail POP3 mailbox
mailbox.mParentKey = Mailbox.NO_MAILBOX;
if (mailbox.isSaved()) {
mailbox.update(mContext, mailbox.toContentValues());
} else {
mailbox.save(mContext);
}
return new Folder[] { getFolder(POP3_MAILBOX_NAME) };
}
/**
* Used by account setup to test if an account's settings are appropriate. The definition
* of "checked" here is simply, can you log into the account and does it meet some minimum set
* of feature requirements?
*
* @throws MessagingException if there was some problem with the account
*/
@Override
public Bundle checkSettings() throws MessagingException {
Pop3Folder folder = new Pop3Folder(POP3_MAILBOX_NAME);
Bundle bundle = null;
// Close any open or half-open connections - checkSettings should always be "fresh"
if (mTransport.isOpen()) {
folder.close(false);
}
try {
folder.open(OpenMode.READ_WRITE);
bundle = folder.checkSettings();
} finally {
folder.close(false); // false == don't expunge anything
}
return bundle;
}
class Pop3Folder extends Folder {
private final HashMap<String, Pop3Message> mUidToMsgMap
= new HashMap<String, Pop3Message>();
private final HashMap<Integer, Pop3Message> mMsgNumToMsgMap
= new HashMap<Integer, Pop3Message>();
private final HashMap<String, Integer> mUidToMsgNumMap = new HashMap<String, Integer>();
private final String mName;
private int mMessageCount;
private Pop3Capabilities mCapabilities;
public Pop3Folder(String name) {
if (name.equalsIgnoreCase(POP3_MAILBOX_NAME)) {
mName = POP3_MAILBOX_NAME;
} else {
mName = name;
}
}
/**
* Used by account setup to test if an account's settings are appropriate. Here, we run
* an additional test to see if UIDL is supported on the server. If it's not we
* can't service this account.
*
* @return Bundle containing validation data (code and, if appropriate, error message)
* @throws MessagingException if the account is not going to be useable
*/
public Bundle checkSettings() throws MessagingException {
Bundle bundle = new Bundle();
int result = MessagingException.NO_ERROR;
if (!mCapabilities.uidl) {
try {
UidlParser parser = new UidlParser();
executeSimpleCommand("UIDL");
// drain the entire output, so additional communications don't get confused.
String response;
while ((response = mTransport.readLine()) != null) {
parser.parseMultiLine(response);
if (parser.mEndOfMessage) {
break;
}
}
} catch (IOException ioe) {
mTransport.close();
result = MessagingException.IOERROR;
bundle.putString(EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE,
ioe.getMessage());
}
}
bundle.putInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE, result);
return bundle;
}
@Override
public synchronized void open(OpenMode mode) throws MessagingException {
if (mTransport.isOpen()) {
return;
}
if (!mName.equalsIgnoreCase(POP3_MAILBOX_NAME)) {
throw new MessagingException("Folder does not exist");
}
try {
mTransport.open();
// Eat the banner
executeSimpleCommand(null);
mCapabilities = getCapabilities();
if (mTransport.canTryTlsSecurity()) {
if (mCapabilities.stls) {
executeSimpleCommand("STLS");
mTransport.reopenTls();
} else {
if (Email.DEBUG) {
Log.d(Logging.LOG_TAG, "TLS not supported but required");
}
throw new MessagingException(MessagingException.TLS_REQUIRED);
}
}
try {
executeSensitiveCommand("USER " + mUsername, "USER /redacted/");
executeSensitiveCommand("PASS " + mPassword, "PASS /redacted/");
} catch (MessagingException me) {
if (Email.DEBUG) {
Log.d(Logging.LOG_TAG, me.toString());
}
throw new AuthenticationFailedException(null, me);
}
} catch (IOException ioe) {
mTransport.close();
if (Email.DEBUG) {
Log.d(Logging.LOG_TAG, ioe.toString());
}
throw new MessagingException(MessagingException.IOERROR, ioe.toString());
}
Exception statException = null;
try {
String response = executeSimpleCommand("STAT");
String[] parts = response.split(" ");
if (parts.length < 2) {
statException = new IOException();
} else {
mMessageCount = Integer.parseInt(parts[1]);
}
} catch (IOException ioe) {
statException = ioe;
} catch (NumberFormatException nfe) {
statException = nfe;
}
if (statException != null) {
mTransport.close();
if (Email.DEBUG) {
Log.d(Logging.LOG_TAG, statException.toString());
}
throw new MessagingException("POP3 STAT", statException);
}
mUidToMsgMap.clear();
mMsgNumToMsgMap.clear();
mUidToMsgNumMap.clear();
}
@Override
public OpenMode getMode() {
return OpenMode.READ_WRITE;
}
/**
* Close the folder (and the transport below it).
*
* MUST NOT return any exceptions.
*
* @param expunge If true all deleted messages will be expunged (TODO - not implemented)
*/
@Override
public void close(boolean expunge) {
try {
executeSimpleCommand("QUIT");
}
catch (Exception e) {
// ignore any problems here - just continue closing
}
mTransport.close();
}
@Override
public String getName() {
return mName;
}
// POP3 does not folder creation
@Override
public boolean canCreate(FolderType type) {
return false;
}
@Override
public boolean create(FolderType type) {
return false;
}
@Override
public boolean exists() {
return mName.equalsIgnoreCase(POP3_MAILBOX_NAME);
}
@Override
public int getMessageCount() {
return mMessageCount;
}
@Override
public int getUnreadMessageCount() {
return -1;
}
@Override
public Message getMessage(String uid) throws MessagingException {
if (mUidToMsgNumMap.size() == 0) {
try {
indexMsgNums(1, mMessageCount);
} catch (IOException ioe) {
mTransport.close();
if (Email.DEBUG) {
Log.d(Logging.LOG_TAG, "Unable to index during getMessage " + ioe);
}
throw new MessagingException("getMessages", ioe);
}
}
Pop3Message message = mUidToMsgMap.get(uid);
return message;
}
@Override
public Message[] getMessages(int start, int end, MessageRetrievalListener listener)
throws MessagingException {
if (start < 1 || end < 1 || end < start) {
throw new MessagingException(String.format("Invalid message set %d %d",
start, end));
}
try {
indexMsgNums(start, end);
} catch (IOException ioe) {
mTransport.close();
if (Email.DEBUG) {
Log.d(Logging.LOG_TAG, ioe.toString());
}
throw new MessagingException("getMessages", ioe);
}
ArrayList<Message> messages = new ArrayList<Message>();
for (int msgNum = start; msgNum <= end; msgNum++) {
Pop3Message message = mMsgNumToMsgMap.get(msgNum);
messages.add(message);
if (listener != null) {
listener.messageRetrieved(message);
}
}
return messages.toArray(new Message[messages.size()]);
}
/**
* Ensures that the given message set (from start to end inclusive)
* has been queried so that uids are available in the local cache.
* @param start
* @param end
* @throws MessagingException
* @throws IOException
*/
private void indexMsgNums(int start, int end)
throws MessagingException, IOException {
int unindexedMessageCount = 0;
for (int msgNum = start; msgNum <= end; msgNum++) {
if (mMsgNumToMsgMap.get(msgNum) == null) {
unindexedMessageCount++;
}
}
if (unindexedMessageCount == 0) {
return;
}
UidlParser parser = new UidlParser();
if (DEBUG_FORCE_SINGLE_LINE_UIDL ||
(unindexedMessageCount < 50 && mMessageCount > 5000)) {
/*
* In extreme cases we'll do a UIDL command per message instead of a bulk
* download.
*/
for (int msgNum = start; msgNum <= end; msgNum++) {
Pop3Message message = mMsgNumToMsgMap.get(msgNum);
if (message == null) {
String response = executeSimpleCommand("UIDL " + msgNum);
if (!parser.parseSingleLine(response)) {
throw new IOException();
}
message = new Pop3Message(parser.mUniqueId, this);
indexMessage(msgNum, message);
}
}
} else {
String response = executeSimpleCommand("UIDL");
while ((response = mTransport.readLine()) != null) {
if (!parser.parseMultiLine(response)) {
throw new IOException();
}
if (parser.mEndOfMessage) {
break;
}
int msgNum = parser.mMessageNumber;
if (msgNum >= start && msgNum <= end) {
Pop3Message message = mMsgNumToMsgMap.get(msgNum);
if (message == null) {
message = new Pop3Message(parser.mUniqueId, this);
indexMessage(msgNum, message);
}
}
}
}
}
private void indexUids(ArrayList<String> uids)
throws MessagingException, IOException {
HashSet<String> unindexedUids = new HashSet<String>();
for (String uid : uids) {
if (mUidToMsgMap.get(uid) == null) {
unindexedUids.add(uid);
}
}
if (unindexedUids.size() == 0) {
return;
}
/*
* If we are missing uids in the cache the only sure way to
* get them is to do a full UIDL list. A possible optimization
* would be trying UIDL for the latest X messages and praying.
*/
UidlParser parser = new UidlParser();
String response = executeSimpleCommand("UIDL");
while ((response = mTransport.readLine()) != null) {
parser.parseMultiLine(response);
if (parser.mEndOfMessage) {
break;
}
if (unindexedUids.contains(parser.mUniqueId)) {
Pop3Message message = mUidToMsgMap.get(parser.mUniqueId);
if (message == null) {
message = new Pop3Message(parser.mUniqueId, this);
}
indexMessage(parser.mMessageNumber, message);
}
}
}
/**
* Simple parser class for UIDL messages.
*
* <p>NOTE: In variance with RFC 1939, we allow multiple whitespace between the
* message-number and unique-id fields. This provides greater compatibility with some
* non-compliant POP3 servers, e.g. mail.comcast.net.
*/
/* package */ class UidlParser {
/**
* Caller can read back message-number from this field
*/
public int mMessageNumber;
/**
* Caller can read back unique-id from this field
*/
public String mUniqueId;
/**
* True if the response was "end-of-message"
*/
public boolean mEndOfMessage;
/**
* True if an error was reported
*/
public boolean mErr;
/**
* Construct & Initialize
*/
public UidlParser() {
mErr = true;
}
/**
* Parse a single-line response. This is returned from a command of the form
* "UIDL msg-num" and will be formatted as: "+OK msg-num unique-id" or
* "-ERR diagnostic text"
*
* @param response The string returned from the server
* @return true if the string parsed as expected (e.g. no syntax problems)
*/
public boolean parseSingleLine(String response) {
mErr = false;
if (response == null || response.length() == 0) {
return false;
}
char first = response.charAt(0);
if (first == '+') {
String[] uidParts = response.split(" +");
if (uidParts.length >= 3) {
try {
mMessageNumber = Integer.parseInt(uidParts[1]);
} catch (NumberFormatException nfe) {
return false;
}
mUniqueId = uidParts[2];
mEndOfMessage = true;
return true;
}
} else if (first == '-') {
mErr = true;
return true;
}
return false;
}
/**
* Parse a multi-line response. This is returned from a command of the form
* "UIDL" and will be formatted as: "." or "msg-num unique-id".
*
* @param response The string returned from the server
* @return true if the string parsed as expected (e.g. no syntax problems)
*/
public boolean parseMultiLine(String response) {
mErr = false;
if (response == null || response.length() == 0) {
return false;
}
char first = response.charAt(0);
if (first == '.') {
mEndOfMessage = true;
return true;
} else {
String[] uidParts = response.split(" +");
if (uidParts.length >= 2) {
try {
mMessageNumber = Integer.parseInt(uidParts[0]);
} catch (NumberFormatException nfe) {
return false;
}
mUniqueId = uidParts[1];
mEndOfMessage = false;
return true;
}
}
return false;
}
}
private void indexMessage(int msgNum, Pop3Message message) {
mMsgNumToMsgMap.put(msgNum, message);
mUidToMsgMap.put(message.getUid(), message);
mUidToMsgNumMap.put(message.getUid(), msgNum);
}
@Override
public Message[] getMessages(String[] uids, MessageRetrievalListener listener) {
throw new UnsupportedOperationException(
"Pop3Folder.getMessage(MessageRetrievalListener)");
}
/**
* Fetch the items contained in the FetchProfile into the given set of
* Messages in as efficient a manner as possible.
* @param messages
* @param fp
* @throws MessagingException
*/
@Override
public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)
throws MessagingException {
if (messages == null || messages.length == 0) {
return;
}
ArrayList<String> uids = new ArrayList<String>();
for (Message message : messages) {
uids.add(message.getUid());
}
try {
indexUids(uids);
if (fp.contains(FetchProfile.Item.ENVELOPE)) {
// Note: We never pass the listener for the ENVELOPE call, because we're going
// to be calling the listener below in the per-message loop.
fetchEnvelope(messages, null);
}
} catch (IOException ioe) {
mTransport.close();
if (Email.DEBUG) {
Log.d(Logging.LOG_TAG, ioe.toString());
}
throw new MessagingException("fetch", ioe);
}
for (int i = 0, count = messages.length; i < count; i++) {
Message message = messages[i];
if (!(message instanceof Pop3Message)) {
throw new MessagingException("Pop3Store.fetch called with non-Pop3 Message");
}
Pop3Message pop3Message = (Pop3Message)message;
try {
if (fp.contains(FetchProfile.Item.BODY)) {
fetchBody(pop3Message, -1);
}
else if (fp.contains(FetchProfile.Item.BODY_SANE)) {
/*
* To convert the suggested download size we take the size
* divided by the maximum line size (76).
*/
fetchBody(pop3Message,
FETCH_BODY_SANE_SUGGESTED_SIZE / 76);
}
else if (fp.contains(FetchProfile.Item.STRUCTURE)) {
/*
* If the user is requesting STRUCTURE we are required to set the body
* to null since we do not support the function.
*/
pop3Message.setBody(null);
}
if (listener != null) {
listener.messageRetrieved(message);
}
} catch (IOException ioe) {
mTransport.close();
if (Email.DEBUG) {
Log.d(Logging.LOG_TAG, ioe.toString());
}
throw new MessagingException("Unable to fetch message", ioe);
}
}
}
private void fetchEnvelope(Message[] messages,
MessageRetrievalListener listener) throws IOException, MessagingException {
int unsizedMessages = 0;
for (Message message : messages) {
if (message.getSize() == -1) {
unsizedMessages++;
}
}
if (unsizedMessages == 0) {
return;
}
if (unsizedMessages < 50 && mMessageCount > 5000) {
/*
* In extreme cases we'll do a command per message instead of a bulk request
* to hopefully save some time and bandwidth.
*/
for (int i = 0, count = messages.length; i < count; i++) {
Message message = messages[i];
if (!(message instanceof Pop3Message)) {
throw new MessagingException("Pop3Store.fetch called with non-Pop3 Message");
}
Pop3Message pop3Message = (Pop3Message)message;
String response = executeSimpleCommand(String.format("LIST %d",
mUidToMsgNumMap.get(pop3Message.getUid())));
try {
String[] listParts = response.split(" ");
int msgNum = Integer.parseInt(listParts[1]);
int msgSize = Integer.parseInt(listParts[2]);
pop3Message.setSize(msgSize);
} catch (NumberFormatException nfe) {
throw new IOException();
}
if (listener != null) {
listener.messageRetrieved(pop3Message);
}
}
} else {
HashSet<String> msgUidIndex = new HashSet<String>();
for (Message message : messages) {
msgUidIndex.add(message.getUid());
}
String response = executeSimpleCommand("LIST");
while ((response = mTransport.readLine()) != null) {
if (response.equals(".")) {
break;
}
Pop3Message pop3Message = null;
int msgSize = 0;
try {
String[] listParts = response.split(" ");
int msgNum = Integer.parseInt(listParts[0]);
msgSize = Integer.parseInt(listParts[1]);
pop3Message = mMsgNumToMsgMap.get(msgNum);
} catch (NumberFormatException nfe) {
throw new IOException();
}
if (pop3Message != null && msgUidIndex.contains(pop3Message.getUid())) {
pop3Message.setSize(msgSize);
if (listener != null) {
listener.messageRetrieved(pop3Message);
}
}
}
}
}
/**
* Fetches the body of the given message, limiting the stored data
* to the specified number of lines. If lines is -1 the entire message
* is fetched. This is implemented with RETR for lines = -1 or TOP
* for any other value. If the server does not support TOP it is
* emulated with RETR and extra lines are thrown away.
*
* Note: Some servers (e.g. live.com) don't support CAPA, but turn out to
* support TOP after all. For better performance on these servers, we'll always
* probe TOP, and fall back to RETR when it's truly unsupported.
*
* @param message
* @param lines
*/
private void fetchBody(Pop3Message message, int lines)
throws IOException, MessagingException {
String response = null;
int messageId = mUidToMsgNumMap.get(message.getUid());
if (lines == -1) {
// Fetch entire message
response = executeSimpleCommand(String.format("RETR %d", messageId));
} else {
// Fetch partial message. Try "TOP", and fall back to slower "RETR" if necessary
try {
response = executeSimpleCommand(String.format("TOP %d %d", messageId, lines));
} catch (MessagingException me) {
response = executeSimpleCommand(String.format("RETR %d", messageId));
}
}
if (response != null) {
try {
InputStream in = mTransport.getInputStream();
if (DEBUG_LOG_RAW_STREAM && Email.DEBUG) {
in = new LoggingInputStream(in);
}
message.parse(new Pop3ResponseInputStream(in));
}
catch (MessagingException me) {
/*
* If we're only downloading headers it's possible
* we'll get a broken MIME message which we're not
* real worried about. If we've downloaded the body
* and can't parse it we need to let the user know.
*/
if (lines == -1) {
throw me;
}
}
}
}
@Override
public Flag[] getPermanentFlags() {
return PERMANENT_FLAGS;
}
@Override
public void appendMessages(Message[] messages) {
}
@Override
public void delete(boolean recurse) {
}
@Override
public Message[] expunge() {
return null;
}
@Override
public void setFlags(Message[] messages, Flag[] flags, boolean value)
throws MessagingException {
if (!value || !Utility.arrayContains(flags, Flag.DELETED)) {
/*
* The only flagging we support is setting the Deleted flag.
*/
return;
}
try {
for (Message message : messages) {
executeSimpleCommand(String.format("DELE %s",
mUidToMsgNumMap.get(message.getUid())));
}
}
catch (IOException ioe) {
mTransport.close();
if (Email.DEBUG) {
Log.d(Logging.LOG_TAG, ioe.toString());
}
throw new MessagingException("setFlags()", ioe);
}
}
@Override
public void copyMessages(Message[] msgs, Folder folder, MessageUpdateCallbacks callbacks) {
throw new UnsupportedOperationException("copyMessages is not supported in POP3");
}
// private boolean isRoundTripModeSuggested() {
// long roundTripMethodMs =
// (uncachedMessageCount * 2 * mLatencyMs);
// long bulkMethodMs =
// (mMessageCount * 58) / (mThroughputKbS * 1024 / 8) * 1000;
// }
private Pop3Capabilities getCapabilities() throws IOException {
Pop3Capabilities capabilities = new Pop3Capabilities();
try {
String response = executeSimpleCommand("CAPA");
while ((response = mTransport.readLine()) != null) {
if (response.equals(".")) {
break;
}
if (response.equalsIgnoreCase("STLS")){
capabilities.stls = true;
}
else if (response.equalsIgnoreCase("UIDL")) {
capabilities.uidl = true;
}
else if (response.equalsIgnoreCase("PIPELINING")) {
capabilities.pipelining = true;
}
else if (response.equalsIgnoreCase("USER")) {
capabilities.user = true;
}
else if (response.equalsIgnoreCase("TOP")) {
capabilities.top = true;
}
}
}
catch (MessagingException me) {
/*
* The server may not support the CAPA command, so we just eat this Exception
* and allow the empty capabilities object to be returned.
*/
}
return capabilities;
}
/**
* Send a single command and wait for a single line response. Reopens the connection,
* if it is closed. Leaves the connection open.
*
* @param command The command string to send to the server.
* @return Returns the response string from the server.
*/
private String executeSimpleCommand(String command) throws IOException, MessagingException {
return executeSensitiveCommand(command, null);
}
/**
* Send a single command and wait for a single line response. Reopens the connection,
* if it is closed. Leaves the connection open.
*
* @param command The command string to send to the server.
* @param sensitiveReplacement If the command includes sensitive data (e.g. authentication)
* please pass a replacement string here (for logging).
* @return Returns the response string from the server.
*/
private String executeSensitiveCommand(String command, String sensitiveReplacement)
throws IOException, MessagingException {
open(OpenMode.READ_WRITE);
if (command != null) {
mTransport.writeLine(command, sensitiveReplacement);
}
String response = mTransport.readLine();
if (response.length() > 1 && response.charAt(0) == '-') {
throw new MessagingException(response);
}
return response;
}
@Override
public boolean equals(Object o) {
if (o instanceof Pop3Folder) {
return ((Pop3Folder) o).mName.equals(mName);
}
return super.equals(o);
}
@Override
public boolean isOpen() {
return mTransport.isOpen();
}
@Override
public Message createMessage(String uid) {
return new Pop3Message(uid, this);
}
@Override
public Message[] getMessages(SearchParams params, MessageRetrievalListener listener)
throws MessagingException {
return null;
}
}
public static class Pop3Message extends MimeMessage {
public Pop3Message(String uid, Pop3Folder folder) {
mUid = uid;
mFolder = folder;
mSize = -1;
}
public void setSize(int size) {
mSize = size;
}
@Override
public void parse(InputStream in) throws IOException, MessagingException {
super.parse(in);
}
@Override
public void setFlag(Flag flag, boolean set) throws MessagingException {
super.setFlag(flag, set);
mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set);
}
}
/**
* POP3 Capabilities as defined in RFC 2449. This is not a complete list of CAPA
* responses - just those that we use in this client.
*/
class Pop3Capabilities {
/** The STLS (start TLS) command is supported */
public boolean stls;
/** the TOP command (retrieve a partial message) is supported */
public boolean top;
/** USER and PASS login/auth commands are supported */
public boolean user;
/** the optional UIDL command is supported (unused) */
public boolean uidl;
/** the server is capable of accepting multiple commands at a time (unused) */
public boolean pipelining;
@Override
public String toString() {
return String.format("STLS %b, TOP %b, USER %b, UIDL %b, PIPELINING %b",
stls,
top,
user,
uidl,
pipelining);
}
}
// TODO figure out what is special about this and merge it into MailTransport
class Pop3ResponseInputStream extends InputStream {
private final InputStream mIn;
private boolean mStartOfLine = true;
private boolean mFinished;
public Pop3ResponseInputStream(InputStream in) {
mIn = in;
}
@Override
public int read() throws IOException {
if (mFinished) {
return -1;
}
int d = mIn.read();
if (mStartOfLine && d == '.') {
d = mIn.read();
if (d == '\r') {
mFinished = true;
mIn.read();
return -1;
}
}
mStartOfLine = (d == '\n');
return d;
}
}
}