Major reorganisation of the Pop3Store to match the other stores

This commit is contained in:
Philip Whitehouse 2017-09-05 01:27:22 +01:00
parent e4467ef959
commit 3cf141553e
16 changed files with 2755 additions and 1118 deletions

View file

@ -0,0 +1,22 @@
package com.fsck.k9.mail.store.pop3;
class Pop3Capabilities {
boolean cramMD5;
boolean authPlain;
boolean stls;
boolean top;
boolean uidl;
boolean external;
@Override
public String toString() {
return String.format("CRAM-MD5 %b, PLAIN %b, STLS %b, TOP %b, UIDL %b, EXTERNAL %b",
cramMD5,
authPlain,
stls,
top,
uidl,
external);
}
}

View file

@ -0,0 +1,26 @@
package com.fsck.k9.mail.store.pop3;
class Pop3Commands {
static final String STLS_COMMAND = "STLS";
static final String USER_COMMAND = "USER";
static final String PASS_COMMAND = "PASS";
static final String CAPA_COMMAND = "CAPA";
static final String AUTH_COMMAND = "AUTH";
static final String STAT_COMMAND = "STAT";
static final String LIST_COMMAND = "LIST";
static final String UIDL_COMMAND = "UIDL";
static final String TOP_COMMAND = "TOP";
static final String RETR_COMMAND = "RETR";
static final String DELE_COMMAND = "DELE";
static final String QUIT_COMMAND = "QUIT";
static final String STLS_CAPABILITY = "STLS";
static final String UIDL_CAPABILITY = "UIDL";
static final String TOP_CAPABILITY = "TOP";
static final String SASL_CAPABILITY = "SASL";
static final String AUTH_PLAIN_CAPABILITY = "PLAIN";
static final String AUTH_CRAM_MD5_CAPABILITY = "CRAM-MD5";
static final String AUTH_EXTERNAL_CAPABILITY = "EXTERNAL";
}

View file

@ -0,0 +1,437 @@
package com.fsck.k9.mail.store.pop3;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.security.GeneralSecurityException;
import java.security.KeyManagementException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import com.fsck.k9.mail.AuthType;
import com.fsck.k9.mail.Authentication;
import com.fsck.k9.mail.AuthenticationFailedException;
import com.fsck.k9.mail.CertificateValidationException;
import com.fsck.k9.mail.ConnectionSecurity;
import com.fsck.k9.mail.K9MailLib;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.filter.Base64;
import com.fsck.k9.mail.filter.Hex;
import com.fsck.k9.mail.ssl.TrustedSocketFactory;
import com.fsck.k9.mail.store.RemoteStore;
import javax.net.ssl.SSLException;
import timber.log.Timber;
import static com.fsck.k9.mail.CertificateValidationException.Reason.MissingCapability;
import static com.fsck.k9.mail.K9MailLib.DEBUG_PROTOCOL_POP3;
import static com.fsck.k9.mail.store.pop3.Pop3Commands.*;
class Pop3Connection {
private final Pop3Settings settings;
private Socket socket;
private BufferedInputStream in;
private BufferedOutputStream out;
private Pop3Capabilities capabilities;
/**
* This value is {@code true} if the server supports the CAPA command but doesn't advertise
* support for the TOP command OR if the server doesn't support the CAPA command and we
* already unsuccessfully tried to use the TOP command.
*/
private boolean topNotAdvertised;
Pop3Connection(Pop3Settings settings,
TrustedSocketFactory trustedSocketFactory) throws MessagingException {
try {
this.settings = settings;
SocketAddress socketAddress = new InetSocketAddress(settings.getHost(), settings.getPort());
if (settings.getConnectionSecurity() == ConnectionSecurity.SSL_TLS_REQUIRED) {
socket = trustedSocketFactory.createSocket(null, settings.getHost(), settings.getPort(), settings.getClientCertificateAlias());
} else {
socket = new Socket();
}
socket.connect(socketAddress, RemoteStore.SOCKET_CONNECT_TIMEOUT);
in = new BufferedInputStream(socket.getInputStream(), 1024);
out = new BufferedOutputStream(socket.getOutputStream(), 512);
socket.setSoTimeout(RemoteStore.SOCKET_READ_TIMEOUT);
if (!isOpen()) {
throw new MessagingException("Unable to connect socket");
}
String serverGreeting = executeSimpleCommand(null);
capabilities = getCapabilities();
if (settings.getConnectionSecurity() == ConnectionSecurity.STARTTLS_REQUIRED) {
performStartTlsUpgrade(trustedSocketFactory, settings.getHost(), settings.getPort(), settings.getClientCertificateAlias());
}
performAuthentication(settings.getAuthType(), serverGreeting);
} catch (SSLException e) {
if (e.getCause() instanceof CertificateException) {
throw new CertificateValidationException(e.getMessage(), e);
} else {
throw new MessagingException("Unable to connect", e);
}
} catch (GeneralSecurityException gse) {
throw new MessagingException(
"Unable to open connection to POP server due to security error.", gse);
} catch (IOException ioe) {
throw new MessagingException("Unable to open connection to POP server.", ioe);
}
}
/*
* If STARTTLS is not available throws a CertificateValidationException which in K-9
* triggers a "Certificate error" notification that takes the user to the incoming
* server settings for review. This might be needed if the account was configured with an obsolete
* "STARTTLS (if available)" setting.
*/
private void performStartTlsUpgrade(TrustedSocketFactory trustedSocketFactory,
String host, int port, String clientCertificateAlias)
throws MessagingException, NoSuchAlgorithmException, KeyManagementException, IOException {
if (capabilities.stls) {
executeSimpleCommand(STLS_COMMAND);
socket = trustedSocketFactory.createSocket(
socket,
host,
port,
clientCertificateAlias);
socket.setSoTimeout(RemoteStore.SOCKET_READ_TIMEOUT);
in = new BufferedInputStream(socket.getInputStream(), 1024);
out = new BufferedOutputStream(socket.getOutputStream(), 512);
if (!isOpen()) {
throw new MessagingException("Unable to connect socket");
}
capabilities = getCapabilities();
} else {
throw new CertificateValidationException(
"STARTTLS connection security not available");
}
}
private void performAuthentication(AuthType authType, String serverGreeting)
throws MessagingException {
switch (authType) {
case PLAIN:
if (capabilities.authPlain) {
authPlain();
} else {
login();
}
break;
case CRAM_MD5:
if (capabilities.cramMD5) {
authCramMD5();
} else {
authAPOP(serverGreeting);
}
break;
case EXTERNAL:
if (capabilities.external) {
authExternal();
} else {
// Provide notification to user of a problem authenticating using client certificates
throw new CertificateValidationException(MissingCapability);
}
break;
default:
throw new MessagingException(
"Unhandled authentication method found in the server settings (bug).");
}
}
boolean isOpen() {
return (in != null && out != null && socket != null
&& socket.isConnected() && !socket.isClosed());
}
private Pop3Capabilities getCapabilities() throws IOException {
Pop3Capabilities capabilities = new Pop3Capabilities();
try {
/*
* Try sending an AUTH command with no arguments.
*
* The server may respond with a list of supported SASL
* authentication mechanisms.
*
* Ref.: http://tools.ietf.org/html/draft-myers-sasl-pop3-05
*
* While this never became a standard, there are servers that
* support it, and Thunderbird includes this check.
*/
executeSimpleCommand(AUTH_COMMAND);
String response;
while ((response = readLine()) != null) {
if (response.equals(".")) {
break;
}
response = response.toUpperCase(Locale.US);
switch (response) {
case AUTH_PLAIN_CAPABILITY:
capabilities.authPlain = true;
break;
case AUTH_CRAM_MD5_CAPABILITY:
capabilities.cramMD5 = true;
break;
case AUTH_EXTERNAL_CAPABILITY:
capabilities.external = true;
break;
}
}
} catch (MessagingException ignored) {
// Assume AUTH command with no arguments is not supported.
}
try {
executeSimpleCommand(CAPA_COMMAND);
String response;
while ((response = readLine()) != null) {
if (response.equals(".")) {
break;
}
response = response.toUpperCase(Locale.US);
if (response.equals(STLS_CAPABILITY)) {
capabilities.stls = true;
} else if (response.equals(UIDL_CAPABILITY)) {
capabilities.uidl = true;
} else if (response.equals(TOP_CAPABILITY)) {
capabilities.top = true;
} else if (response.startsWith(SASL_CAPABILITY)) {
List<String> saslAuthMechanisms = Arrays.asList(response.split(" "));
if (saslAuthMechanisms.contains(AUTH_PLAIN_CAPABILITY)) {
capabilities.authPlain = true;
}
if (saslAuthMechanisms.contains(AUTH_CRAM_MD5_CAPABILITY)) {
capabilities.cramMD5 = true;
}
}
}
if (!capabilities.top) {
/*
* If the CAPA command is supported but it doesn't advertise support for the
* TOP command, we won't check for it manually.
*/
topNotAdvertised = 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;
}
private void login() throws MessagingException {
executeSimpleCommand(USER_COMMAND + " " + settings.getUsername());
try {
executeSimpleCommand(PASS_COMMAND + " " + settings.getPassword(), true);
} catch (Pop3ErrorResponse e) {
throw new AuthenticationFailedException(
"POP3 login authentication failed: " + e.getMessage(), e);
}
}
private void authPlain() throws MessagingException {
executeSimpleCommand("AUTH PLAIN");
try {
byte[] encodedBytes = Base64.encodeBase64(("\000" + settings.getUsername()
+ "\000" + settings.getPassword()).getBytes());
executeSimpleCommand(new String(encodedBytes), true);
} catch (Pop3ErrorResponse e) {
throw new AuthenticationFailedException(
"POP3 SASL auth PLAIN authentication failed: "
+ e.getMessage(), e);
}
}
private void authAPOP(String serverGreeting) throws MessagingException {
// regex based on RFC 2449 (3.) "Greeting"
String timestamp = serverGreeting.replaceFirst(
"^\\+OK *(?:\\[[^\\]]+\\])?[^<]*(<[^>]*>)?[^<]*$", "$1");
if ("".equals(timestamp)) {
throw new MessagingException(
"APOP authentication is not supported");
}
MessageDigest md;
try {
md = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new MessagingException(
"MD5 failure during POP3 auth APOP", e);
}
byte[] digest = md.digest((timestamp + settings.getPassword()).getBytes());
String hexDigest = Hex.encodeHex(digest);
try {
executeSimpleCommand("APOP " + settings.getUsername() + " " + hexDigest, true);
} catch (Pop3ErrorResponse e) {
throw new AuthenticationFailedException(
"POP3 APOP authentication failed: " + e.getMessage(), e);
}
}
private void authCramMD5() throws MessagingException {
String b64Nonce = executeSimpleCommand("AUTH CRAM-MD5").replace("+ ", "");
String b64CRAM = Authentication.computeCramMd5(settings.getUsername(), settings.getPassword(), b64Nonce);
try {
executeSimpleCommand(b64CRAM, true);
} catch (Pop3ErrorResponse e) {
throw new AuthenticationFailedException(
"POP3 CRAM-MD5 authentication failed: "
+ e.getMessage(), e);
}
}
private void authExternal() throws MessagingException {
try {
executeSimpleCommand(
String.format("AUTH EXTERNAL %s",
Base64.encode(settings.getUsername())), false);
} catch (Pop3ErrorResponse e) {
/*
* Provide notification to the user of a problem authenticating
* using client certificates. We don't use an
* AuthenticationFailedException because that would trigger a
* "Username or password incorrect" notification in
* AccountSetupCheckSettings.
*/
throw new CertificateValidationException(
"POP3 client certificate authentication failed: " + e.getMessage(), e);
}
}
private void writeLine(String s) throws IOException {
out.write(s.getBytes());
out.write('\r');
out.write('\n');
out.flush();
}
String executeSimpleCommand(String command) throws MessagingException {
return executeSimpleCommand(command, false);
}
private String executeSimpleCommand(String command, boolean sensitive) throws MessagingException {
try {
if (command != null) {
if (K9MailLib.isDebug() && DEBUG_PROTOCOL_POP3) {
if (sensitive && !K9MailLib.isDebugSensitive()) {
Timber.d(">>> [Command Hidden, Enable Sensitive Debug Logging To Show]");
} else {
Timber.d(">>> %s", command);
}
}
writeLine(command);
}
String response = readLine();
if (response.length() == 0 || response.charAt(0) != '+') {
throw new Pop3ErrorResponse(response);
}
return response;
} catch (MessagingException me) {
throw me;
} catch (Exception e) {
close();
throw new MessagingException("Unable to execute POP3 command", e);
}
}
String readLine() throws IOException {
StringBuilder sb = new StringBuilder();
int d = in.read();
if (d == -1) {
throw new IOException("End of stream reached while trying to read line.");
}
do {
if (((char)d) == '\r') {
//noinspection UnnecessaryContinue Makes it easier to follow
continue;
} else if (((char)d) == '\n') {
break;
} else {
sb.append((char)d);
}
} while ((d = in.read()) != -1);
String ret = sb.toString();
if (K9MailLib.isDebug() && DEBUG_PROTOCOL_POP3) {
Timber.d("<<< %s", ret);
}
return ret;
}
void close() {
try {
in.close();
} catch (Exception e) {
/*
* May fail if the connection is already closed.
*/
}
try {
out.close();
} catch (Exception e) {
/*
* May fail if the connection is already closed.
*/
}
try {
socket.close();
} catch (Exception e) {
/*
* May fail if the connection is already closed.
*/
}
in = null;
out = null;
socket = null;
}
boolean supportsTop() {
return capabilities.top;
}
boolean isTopNotAdvertised() {
return topNotAdvertised;
}
void setSupportsTop(boolean supportsTop) {
this.capabilities.top = supportsTop;
}
void setTopNotAdvertised(boolean topNotAdvertised) {
this.topNotAdvertised = topNotAdvertised;
}
boolean supportsUidl() {
return this.capabilities.uidl;
}
InputStream getInputStream() {
return in;
}
}

View file

@ -0,0 +1,16 @@
package com.fsck.k9.mail.store.pop3;
import com.fsck.k9.mail.MessagingException;
/**
* Exception that is thrown if the server returns an error response.
*/
class Pop3ErrorResponse extends MessagingException {
private static final long serialVersionUID = 3672087845857867174L;
public Pop3ErrorResponse(String message) {
super(message, true);
}
}

View file

@ -0,0 +1,610 @@
package com.fsck.k9.mail.store.pop3;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import android.annotation.SuppressLint;
import com.fsck.k9.mail.FetchProfile;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Folder;
import com.fsck.k9.mail.K9MailLib;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessageRetrievalListener;
import com.fsck.k9.mail.MessagingException;
import timber.log.Timber;
import static com.fsck.k9.mail.K9MailLib.DEBUG_PROTOCOL_POP3;
import static com.fsck.k9.mail.store.pop3.Pop3Commands.*;
/**
* POP3 only supports one folder, "Inbox". So the folder name is the ID here.
*/
class Pop3Folder extends Folder<Pop3Message> {
private Pop3Store pop3Store;
private Map<String, Pop3Message> uidToMsgMap = new HashMap<>();
@SuppressLint("UseSparseArrays")
private Map<Integer, Pop3Message> msgNumToMsgMap = new HashMap<>();
private Map<String, Integer> uidToMsgNumMap = new HashMap<>();
private String name;
private int messageCount;
private Pop3Connection connection;
Pop3Folder(Pop3Store pop3Store, String name) {
super();
this.pop3Store = pop3Store;
this.name = name;
if (this.name.equalsIgnoreCase(pop3Store.getConfig().getInboxFolderName())) {
this.name = pop3Store.getConfig().getInboxFolderName();
}
}
@Override
public synchronized void open(int mode) throws MessagingException {
if (isOpen()) {
return;
}
if (!name.equalsIgnoreCase(pop3Store.getConfig().getInboxFolderName())) {
throw new MessagingException("Folder does not exist");
}
connection = pop3Store.createConnection();
String response = connection.executeSimpleCommand(STAT_COMMAND);
String[] parts = response.split(" ");
messageCount = Integer.parseInt(parts[1]);
uidToMsgMap.clear();
msgNumToMsgMap.clear();
uidToMsgNumMap.clear();
}
@Override
public boolean isOpen() {
return connection != null && connection.isOpen();
}
@Override
public int getMode() {
return Folder.OPEN_MODE_RW;
}
@Override
public void close() {
try {
if (isOpen()) {
connection.executeSimpleCommand(QUIT_COMMAND);
}
} catch (Exception e) {
/*
* QUIT may fail if the connection is already closed. We don't care. It's just
* being friendly.
*/
}
if (connection != null) {
connection.close();
connection = null;
}
}
@Override
public String getName() {
return name;
}
@Override
public boolean create(FolderType type) throws MessagingException {
return false;
}
@Override
public boolean exists() throws MessagingException {
return name.equalsIgnoreCase(pop3Store.getConfig().getInboxFolderName());
}
@Override
public int getMessageCount() {
return messageCount;
}
@Override
public int getUnreadMessageCount() throws MessagingException {
return -1;
}
@Override
public int getFlaggedMessageCount() throws MessagingException {
return -1;
}
@Override
public Pop3Message getMessage(String uid) throws MessagingException {
Pop3Message message = uidToMsgMap.get(uid);
if (message == null) {
message = new Pop3Message(uid, this);
}
return message;
}
@Override
public List<Pop3Message> getMessages(int start, int end, Date earliestDate, MessageRetrievalListener<Pop3Message> listener)
throws MessagingException {
if (start < 1 || end < 1 || end < start) {
throw new MessagingException(String.format(Locale.US, "Invalid message set %d %d",
start, end));
}
try {
indexMsgNums(start, end);
} catch (IOException ioe) {
throw new MessagingException("getMessages", ioe);
}
List<Pop3Message> messages = new ArrayList<>();
int i = 0;
for (int msgNum = start; msgNum <= end; msgNum++) {
Pop3Message message = msgNumToMsgMap.get(msgNum);
if (message == null) {
/*
* There could be gaps in the message numbers or malformed
* responses which lead to "gaps" in msgNumToMsgMap.
*
* See issue 2252
*/
continue;
}
if (listener != null) {
listener.messageStarted(message.getUid(), i++, (end - start) + 1);
}
messages.add(message);
if (listener != null) {
listener.messageFinished(message, i++, (end - start) + 1);
}
}
return messages;
}
@Override
public boolean areMoreMessagesAvailable(int indexOfOldestMessage, Date earliestDate) {
return indexOfOldestMessage > 1;
}
/**
* Ensures that the given message set (from start to end inclusive)
* has been queried so that uids are available in the local cache.
*/
private void indexMsgNums(int start, int end) throws MessagingException, IOException {
int unindexedMessageCount = 0;
for (int msgNum = start; msgNum <= end; msgNum++) {
if (msgNumToMsgMap.get(msgNum) == null) {
unindexedMessageCount++;
}
}
if (unindexedMessageCount == 0) {
return;
}
if (unindexedMessageCount < 50 && messageCount > 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 = msgNumToMsgMap.get(msgNum);
if (message == null) {
String response = connection.executeSimpleCommand(UIDL_COMMAND + " " + msgNum);
// response = "+OK msgNum msgUid"
String[] uidParts = response.split(" +");
if (uidParts.length < 3 || !"+OK".equals(uidParts[0])) {
Timber.e("ERR response: %s", response);
return;
}
String msgUid = uidParts[2];
message = new Pop3Message(msgUid, this);
indexMessage(msgNum, message);
}
}
} else {
connection.executeSimpleCommand(UIDL_COMMAND);
String response;
while ((response = connection.readLine()) != null) {
if (response.equals(".")) {
break;
}
/*
* Yet another work-around for buggy server software:
* split the response into message number and unique identifier, no matter how many spaces it has
*
* Example for a malformed response:
* 1 2011071307115510400ae3e9e00bmu9
*
* Note the three spaces between message number and unique identifier.
* See issue 3546
*/
String[] uidParts = response.split(" +");
if ((uidParts.length >= 3) && "+OK".equals(uidParts[0])) {
/*
* At least one server software places a "+OK" in
* front of every line in the unique-id listing.
*
* Fix up the array if we detected this behavior.
* See Issue 1237
*/
uidParts[0] = uidParts[1];
uidParts[1] = uidParts[2];
}
if (uidParts.length >= 2) {
Integer msgNum = Integer.valueOf(uidParts[0]);
String msgUid = uidParts[1];
if (msgNum >= start && msgNum <= end) {
Pop3Message message = msgNumToMsgMap.get(msgNum);
if (message == null) {
message = new Pop3Message(msgUid, this);
indexMessage(msgNum, message);
}
}
}
}
}
}
private void indexUids(List<String> uids)
throws MessagingException, IOException {
Set<String> unindexedUids = new HashSet<>();
for (String uid : uids) {
if (uidToMsgMap.get(uid) == null) {
if (K9MailLib.isDebug() && DEBUG_PROTOCOL_POP3) {
Timber.d("Need to index UID %s", uid);
}
unindexedUids.add(uid);
}
}
if (unindexedUids.isEmpty()) {
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.
*/
connection.executeSimpleCommand(UIDL_COMMAND);
String response;
while ((response = connection.readLine()) != null) {
if (response.equals(".")) {
break;
}
String[] uidParts = response.split(" +");
// Ignore messages without a unique-id
if (uidParts.length >= 2) {
Integer msgNum = Integer.valueOf(uidParts[0]);
String msgUid = uidParts[1];
if (unindexedUids.contains(msgUid)) {
if (K9MailLib.isDebug() && DEBUG_PROTOCOL_POP3) {
Timber.d("Got msgNum %d for UID %s", msgNum, msgUid);
}
Pop3Message message = uidToMsgMap.get(msgUid);
if (message == null) {
message = new Pop3Message(msgUid, this);
}
indexMessage(msgNum, message);
}
}
}
}
private void indexMessage(int msgNum, Pop3Message message) {
if (K9MailLib.isDebug() && DEBUG_PROTOCOL_POP3) {
Timber.d("Adding index for UID %s to msgNum %d", message.getUid(), msgNum);
}
msgNumToMsgMap.put(msgNum, message);
uidToMsgMap.put(message.getUid(), message);
uidToMsgNumMap.put(message.getUid(), msgNum);
}
/**
* Fetch the items contained in the FetchProfile into the given set of
* Messages in as efficient a manner as possible.
* @param messages Messages to populate
* @param fp The contents to populate
*/
@Override
public void fetch(List<Pop3Message> messages, FetchProfile fp,
MessageRetrievalListener<Pop3Message> listener)
throws MessagingException {
if (messages == null || messages.isEmpty()) {
return;
}
List<String> uids = new ArrayList<>();
for (Message message : messages) {
uids.add(message.getUid());
}
try {
indexUids(uids);
} catch (IOException ioe) {
throw new MessagingException("fetch", ioe);
}
try {
if (fp.contains(FetchProfile.Item.ENVELOPE)) {
/*
* We pass the listener only if there are other things to do in the
* FetchProfile. Since fetchEnvelop works in bulk and eveything else
* works one at a time if we let fetchEnvelope send events the
* event would get sent twice.
*/
fetchEnvelope(messages, fp.size() == 1 ? listener : null);
}
} catch (IOException ioe) {
throw new MessagingException("fetch", ioe);
}
for (int i = 0, count = messages.size(); i < count; i++) {
Pop3Message pop3Message = messages.get(i);
try {
if (listener != null && !fp.contains(FetchProfile.Item.ENVELOPE)) {
listener.messageStarted(pop3Message.getUid(), i, count);
}
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).
*/
if (pop3Store.getConfig().getMaximumAutoDownloadMessageSize() > 0) {
fetchBody(pop3Message,
(pop3Store.getConfig().getMaximumAutoDownloadMessageSize() / 76));
} else {
fetchBody(pop3Message, -1);
}
} 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 && !(fp.contains(FetchProfile.Item.ENVELOPE) && fp.size() == 1)) {
listener.messageFinished(pop3Message, i, count);
}
} catch (IOException ioe) {
throw new MessagingException("Unable to fetch message", ioe);
}
}
}
private void fetchEnvelope(List<Pop3Message> messages,
MessageRetrievalListener<Pop3Message> listener) throws IOException, MessagingException {
int unsizedMessages = 0;
for (Message message : messages) {
if (message.getSize() == -1) {
unsizedMessages++;
}
}
if (unsizedMessages == 0) {
return;
}
if (unsizedMessages < 50 && messageCount > 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.size(); i < count; i++) {
Pop3Message message = messages.get(i);
if (listener != null) {
listener.messageStarted(message.getUid(), i, count);
}
String response = connection.executeSimpleCommand(
String.format(Locale.US, LIST_COMMAND + " %d",
uidToMsgNumMap.get(message.getUid())));
String[] listParts = response.split(" ");
//int msgNum = Integer.parseInt(listParts[1]);
int msgSize = Integer.parseInt(listParts[2]);
message.setSize(msgSize);
if (listener != null) {
listener.messageFinished(message, i, count);
}
}
} else {
Set<String> msgUidIndex = new HashSet<>();
for (Message message : messages) {
msgUidIndex.add(message.getUid());
}
int i = 0, count = messages.size();
connection.executeSimpleCommand(LIST_COMMAND);
String response;
while ((response = connection.readLine()) != null) {
if (response.equals(".")) {
break;
}
String[] listParts = response.split(" ");
int msgNum = Integer.parseInt(listParts[0]);
int msgSize = Integer.parseInt(listParts[1]);
Pop3Message pop3Message = msgNumToMsgMap.get(msgNum);
if (pop3Message != null && msgUidIndex.contains(pop3Message.getUid())) {
if (listener != null) {
listener.messageStarted(pop3Message.getUid(), i, count);
}
pop3Message.setSize(msgSize);
if (listener != null) {
listener.messageFinished(pop3Message, i, count);
}
i++;
}
}
}
}
/**
* Fetches the body of the given message, limiting the downloaded data to the specified
* number of lines if possible.
*
* 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, RETR is used
* instead.
*/
private void fetchBody(Pop3Message message, int lines)
throws IOException, MessagingException {
String response = null;
// Try hard to use the TOP command if we're not asked to download the whole message.
if (lines != -1 && (!connection.isTopNotAdvertised() || connection.supportsTop())) {
try {
if (K9MailLib.isDebug() && DEBUG_PROTOCOL_POP3 && !connection.supportsTop()) {
Timber.d("This server doesn't support the CAPA command. " +
"Checking to see if the TOP command is supported nevertheless.");
}
response = connection.executeSimpleCommand(
String.format(Locale.US, TOP_COMMAND + " %d %d",
uidToMsgNumMap.get(message.getUid()), lines));
// TOP command is supported. Remember this for the next time.
connection.setSupportsTop(true);
} catch (Pop3ErrorResponse e) {
if (connection.supportsTop()) {
// The TOP command should be supported but something went wrong.
throw e;
} else {
if (K9MailLib.isDebug() && DEBUG_PROTOCOL_POP3) {
Timber.d("The server really doesn't support the TOP " +
"command. Using RETR instead.");
}
// Don't try to use the TOP command again.
connection.setTopNotAdvertised(false);
}
}
}
if (response == null) {
connection.executeSimpleCommand(String.format(Locale.US, RETR_COMMAND + " %d",
uidToMsgNumMap.get(message.getUid())));
}
try {
message.parse(new Pop3ResponseInputStream(connection.getInputStream()));
// TODO: if we've received fewer lines than requested we also have the complete message.
if (lines == -1 || !connection.supportsTop()) {
message.setFlag(Flag.X_DOWNLOADED_FULL, true);
}
} 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 Map<String, String> appendMessages(List<? extends Message> messages) throws MessagingException {
return null;
}
@Override
public void delete(boolean recurse) throws MessagingException {
}
@Override
public void delete(List<? extends Message> msgs, String trashFolderName) throws MessagingException {
setFlags(msgs, Collections.singleton(Flag.DELETED), true);
}
@Override
public String getUidFromMessageId(Message message) throws MessagingException {
return null;
}
@Override
public void setFlags(final Set<Flag> flags, boolean value) throws MessagingException {
throw new UnsupportedOperationException("POP3: No setFlags(Set<Flag>,boolean)");
}
@Override
public void setFlags(List<? extends Message> messages, final Set<Flag> flags, boolean value)
throws MessagingException {
if (!value || !flags.contains(Flag.DELETED)) {
/*
* The only flagging we support is setting the Deleted flag.
*/
return;
}
List<String> uids = new ArrayList<>();
try {
for (Message message : messages) {
uids.add(message.getUid());
}
indexUids(uids);
} catch (IOException ioe) {
throw new MessagingException("Could not get message number for uid " + uids, ioe);
}
for (Message message : messages) {
Integer msgNum = uidToMsgNumMap.get(message.getUid());
if (msgNum == null) {
MessagingException me = new MessagingException("Could not delete message " + message.getUid()
+ " because no msgNum found; permanent error");
me.setPermanentFailure(true);
throw me;
}
open(Folder.OPEN_MODE_RW);
connection.executeSimpleCommand(String.format(DELE_COMMAND + " %s", msgNum));
}
}
@Override
public boolean isFlagSupported(Flag flag) {
return (flag == Flag.DELETED);
}
@Override
public boolean supportsFetchingFlags() {
return false;
}
@Override
public boolean equals(Object o) {
if (o instanceof Pop3Folder) {
return ((Pop3Folder) o).name.equals(name);
}
return super.equals(o);
}
@Override
public int hashCode() {
return name.hashCode();
}
void requestUidl() throws MessagingException {
if (!connection.supportsUidl()) {
/*
* Run an additional test to see if UIDL is supported on the server. If it's not we
* can't service this account.
*/
/*
* If the server doesn't support UIDL it will return a - response, which causes
* executeSimpleCommand to throw a MessagingException, exiting this method.
*/
connection.executeSimpleCommand(UIDL_COMMAND);
}
}
}

View file

@ -0,0 +1,40 @@
package com.fsck.k9.mail.store.pop3;
import java.util.Collections;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.internet.MimeMessage;
class Pop3Message extends MimeMessage {
Pop3Message(String uid, Pop3Folder folder) {
mUid = uid;
mFolder = folder;
mSize = -1;
}
public void setSize(int size) {
mSize = size;
}
@Override
public void setFlag(Flag flag, boolean set) throws MessagingException {
super.setFlag(flag, set);
mFolder.setFlags(Collections.singletonList(this), Collections.singleton(flag), set);
}
@Override
public void delete(String trashFolderName) throws MessagingException {
// try
// {
// Poor POP3 users, we can't copy the message to the Trash folder, but they still want a delete
setFlag(Flag.DELETED, true);
// }
// catch (MessagingException me)
// {
// Log.w(LOG_TAG, "Could not delete non-existent message", me);
// }
}
}

View file

@ -0,0 +1,36 @@
package com.fsck.k9.mail.store.pop3;
import java.io.IOException;
import java.io.InputStream;
class Pop3ResponseInputStream extends InputStream {
private InputStream mIn;
private boolean mStartOfLine = true;
private boolean mFinished;
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;
}
}

View file

@ -0,0 +1,22 @@
package com.fsck.k9.mail.store.pop3;
import com.fsck.k9.mail.AuthType;
import com.fsck.k9.mail.ConnectionSecurity;
interface Pop3Settings {
String getHost();
int getPort();
ConnectionSecurity getConnectionSecurity();
AuthType getAuthType();
String getUsername();
String getPassword();
String getClientCertificateAlias();
}

View file

@ -0,0 +1,423 @@
package com.fsck.k9.mail.store.pop3;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.Deque;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
import android.annotation.SuppressLint;
import com.fsck.k9.mail.helpers.KeyStoreProvider;
import com.jcraft.jzlib.JZlib;
import com.jcraft.jzlib.ZOutputStream;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import okio.BufferedSink;
import okio.BufferedSource;
import okio.Okio;
import org.apache.commons.io.IOUtils;
@SuppressLint("NewApi")
public class MockPop3Server {
private static final byte[] CRLF = { '\r', '\n' };
private final Deque<ImapInteraction> interactions = new ConcurrentLinkedDeque<>();
private final CountDownLatch waitForConnectionClosed = new CountDownLatch(1);
private final CountDownLatch waitForAllExpectedCommands = new CountDownLatch(1);
private final KeyStoreProvider keyStoreProvider;
private final Logger logger;
private MockServerThread mockServerThread;
private String host;
private int port;
public MockPop3Server() {
this(KeyStoreProvider.getInstance(), new DefaultLogger());
}
public MockPop3Server(KeyStoreProvider keyStoreProvider, Logger logger) {
this.keyStoreProvider = keyStoreProvider;
this.logger = logger;
}
public void output(String response) {
checkServerNotRunning();
interactions.add(new CannedResponse(response));
}
public void expect(String command) {
checkServerNotRunning();
interactions.add(new ExpectedCommand(command));
}
public void startTls() {
checkServerNotRunning();
interactions.add(new UpgradeToTls());
}
public void enableCompression() {
checkServerNotRunning();
interactions.add(new EnableCompression());
}
public void closeConnection() {
checkServerNotRunning();
interactions.add(new CloseConnection());
}
public void start() throws IOException {
checkServerNotRunning();
InetAddress localAddress = InetAddress.getByName(null);
ServerSocket serverSocket = new ServerSocket(0, 1, localAddress);
InetSocketAddress localSocketAddress = (InetSocketAddress) serverSocket.getLocalSocketAddress();
host = localSocketAddress.getHostString();
port = serverSocket.getLocalPort();
mockServerThread = new MockServerThread(serverSocket, interactions, waitForConnectionClosed,
waitForAllExpectedCommands, logger, keyStoreProvider);
mockServerThread.start();
}
public void shutdown() {
checkServerRunning();
mockServerThread.shouldStop();
waitForMockServerThread();
}
private void waitForMockServerThread() {
try {
mockServerThread.join(500L);
} catch (InterruptedException ignored) {
}
}
public String getHost() {
checkServerRunning();
return host;
}
public int getPort() {
checkServerRunning();
return port;
}
public void waitForInteractionToComplete() {
checkServerRunning();
try {
waitForAllExpectedCommands.await(1000L, TimeUnit.MILLISECONDS);
} catch (InterruptedException ignored) {
}
}
public void verifyInteractionCompleted() {
shutdown();
if (!interactions.isEmpty()) {
throw new AssertionError("Interactions left: " + interactions.size());
}
UnexpectedCommandException unexpectedCommandException = mockServerThread.getUnexpectedCommandException();
if (unexpectedCommandException != null) {
throw new AssertionError(unexpectedCommandException.getMessage(), unexpectedCommandException);
}
}
public void verifyConnectionStillOpen() {
checkServerRunning();
if (mockServerThread.isClientConnectionClosed()) {
throw new AssertionError("Connection closed when it shouldn't be");
}
}
public void verifyConnectionClosed() {
checkServerRunning();
try {
waitForConnectionClosed.await(300L, TimeUnit.MILLISECONDS);
} catch (InterruptedException ignored) {
}
if (!mockServerThread.isClientConnectionClosed()) {
throw new AssertionError("Connection open when is shouldn't be");
}
}
private void checkServerRunning() {
if (mockServerThread == null) {
throw new IllegalStateException("Server was never started");
}
}
private void checkServerNotRunning() {
if (mockServerThread != null) {
throw new IllegalStateException("Server was already started");
}
}
public interface Logger {
void log(String message);
void log(String format, Object... args);
}
private interface ImapInteraction {}
private static class ExpectedCommand implements ImapInteraction {
private final String command;
public ExpectedCommand(String command) {
this.command = command;
}
public String getCommand() {
return command;
}
}
private static class CannedResponse implements ImapInteraction {
private final String response;
public CannedResponse(String response) {
this.response = response;
}
public String getResponse() {
return response;
}
}
private static class CloseConnection implements ImapInteraction {
}
private static class EnableCompression implements ImapInteraction {
}
private static class UpgradeToTls implements ImapInteraction {
}
private static class UnexpectedCommandException extends Exception {
public UnexpectedCommandException(String expectedCommand, String receivedCommand) {
super("Expected <" + expectedCommand + ">, but received <" + receivedCommand + ">");
}
}
private static class MockServerThread extends Thread {
private final ServerSocket serverSocket;
private final Deque<ImapInteraction> interactions;
private final CountDownLatch waitForConnectionClosed;
private final CountDownLatch waitForAllExpectedCommands;
private final Logger logger;
private final KeyStoreProvider keyStoreProvider;
private volatile boolean shouldStop = false;
private volatile Socket clientSocket;
private BufferedSource input;
private BufferedSink output;
private volatile UnexpectedCommandException unexpectedCommandException;
public MockServerThread(ServerSocket serverSocket, Deque<ImapInteraction> interactions,
CountDownLatch waitForConnectionClosed, CountDownLatch waitForAllExpectedCommands, Logger logger,
KeyStoreProvider keyStoreProvider) {
super("MockImapServer");
this.serverSocket = serverSocket;
this.interactions = interactions;
this.waitForConnectionClosed = waitForConnectionClosed;
this.waitForAllExpectedCommands = waitForAllExpectedCommands;
this.logger = logger;
this.keyStoreProvider = keyStoreProvider;
}
@Override
public void run() {
String hostAddress = serverSocket.getInetAddress().getHostAddress();
int port = serverSocket.getLocalPort();
logger.log("Listening on %s:%d", hostAddress, port);
Socket socket = null;
try {
socket = acceptConnectionAndCloseServerSocket();
clientSocket = socket;
String remoteHostAddress = socket.getInetAddress().getHostAddress();
int remotePort = socket.getPort();
logger.log("Accepted connection from %s:%d", remoteHostAddress, remotePort);
input = Okio.buffer(Okio.source(socket));
output = Okio.buffer(Okio.sink(socket));
while (!shouldStop && !interactions.isEmpty()) {
handleInteractions(socket);
}
waitForAllExpectedCommands.countDown();
while (!shouldStop) {
readAdditionalCommands();
}
waitForConnectionClosed.countDown();
} catch (UnexpectedCommandException e) {
unexpectedCommandException = e;
} catch (IOException e) {
if (!shouldStop) {
logger.log("Exception: %s", e);
}
} catch (KeyStoreException | CertificateException | UnrecoverableKeyException |
NoSuchAlgorithmException | KeyManagementException e) {
throw new RuntimeException(e);
}
IOUtils.closeQuietly(socket);
logger.log("Exiting");
}
private void handleInteractions(Socket socket) throws IOException, KeyStoreException,
NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException, KeyManagementException,
UnexpectedCommandException {
ImapInteraction interaction = interactions.pop();
if (interaction instanceof ExpectedCommand) {
readExpectedCommand((ExpectedCommand) interaction);
} else if (interaction instanceof CannedResponse) {
writeCannedResponse((CannedResponse) interaction);
} else if (interaction instanceof CloseConnection) {
clientSocket.close();
} else if (interaction instanceof EnableCompression) {
enableCompression(socket);
} else if (interaction instanceof UpgradeToTls) {
upgradeToTls(socket);
}
}
private void readExpectedCommand(ExpectedCommand expectedCommand) throws IOException,
UnexpectedCommandException {
String command = input.readUtf8Line();
if (command == null) {
throw new EOFException();
}
logger.log("C: %s", command);
String expected = expectedCommand.getCommand();
if (!command.equals(expected)) {
logger.log("EXPECTED: %s", expected);
throw new UnexpectedCommandException(expected, command);
}
}
private void writeCannedResponse(CannedResponse cannedResponse) throws IOException {
String response = cannedResponse.getResponse();
logger.log("S: %s", response);
output.writeUtf8(response);
output.write(CRLF);
output.flush();
}
private void enableCompression(Socket socket) throws IOException {
InputStream inputStream = new InflaterInputStream(socket.getInputStream(), new Inflater(true));
input = Okio.buffer(Okio.source(inputStream));
ZOutputStream outputStream = new ZOutputStream(socket.getOutputStream(), JZlib.Z_BEST_SPEED, true);
outputStream.setFlushMode(JZlib.Z_PARTIAL_FLUSH);
output = Okio.buffer(Okio.sink(outputStream));
}
private void upgradeToTls(Socket socket) throws KeyStoreException, IOException, NoSuchAlgorithmException,
CertificateException, UnrecoverableKeyException, KeyManagementException {
KeyStore keyStore = keyStoreProvider.getKeyStore();
String defaultAlgorithm = KeyManagerFactory.getDefaultAlgorithm();
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(defaultAlgorithm);
keyManagerFactory.init(keyStore, keyStoreProvider.getPassword());
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagerFactory.getKeyManagers(), null, null);
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(
socket, socket.getInetAddress().getHostAddress(), socket.getPort(), true);
sslSocket.setUseClientMode(false);
sslSocket.startHandshake();
input = Okio.buffer(Okio.source(sslSocket.getInputStream()));
output = Okio.buffer(Okio.sink(sslSocket.getOutputStream()));
}
private void readAdditionalCommands() throws IOException {
String command = input.readUtf8Line();
if (command == null) {
throw new EOFException();
}
logger.log("Received additional command: %s", command);
}
private Socket acceptConnectionAndCloseServerSocket() throws IOException {
Socket socket = serverSocket.accept();
serverSocket.close();
return socket;
}
public void shouldStop() {
shouldStop = true;
IOUtils.closeQuietly(clientSocket);
}
public boolean isClientConnectionClosed() {
return clientSocket.isClosed();
}
public UnexpectedCommandException getUnexpectedCommandException() {
return unexpectedCommandException;
}
}
private static class DefaultLogger implements Logger {
@Override
public void log(String message) {
System.out.println("MockPop3Server: " + message);
}
@Override
public void log(String format, Object... args) {
log(String.format(format, args));
}
}
}

View file

@ -0,0 +1,19 @@
package com.fsck.k9.mail.store.pop3;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class Pop3CapabilitiesTest {
@Test
public void toString_producesReadableOutput() {
String result = new Pop3Capabilities().toString();;
assertEquals(
"CRAM-MD5 false, PLAIN false, STLS false, TOP false, UIDL false, EXTERNAL false",
result);
}
}

View file

@ -0,0 +1,490 @@
package com.fsck.k9.mail.store.pop3;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import com.fsck.k9.mail.AuthType;
import com.fsck.k9.mail.AuthenticationFailedException;
import com.fsck.k9.mail.CertificateValidationException;
import com.fsck.k9.mail.CertificateValidationException.Reason;
import com.fsck.k9.mail.ConnectionSecurity;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.filter.Base64;
import com.fsck.k9.mail.helpers.TestTrustedSocketFactory;
import com.fsck.k9.mail.ssl.TrustedSocketFactory;
import javax.net.ssl.SSLException;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class Pop3ConnectionTest {
private static final String host = "server";
private static final int port = 12345;
private static String username = "user";
private static String password = "password";
private static final String INITIAL_RESPONSE = "+OK POP3 server greeting\r\n";
private static final String AUTH = "AUTH\r\n";
private static final String AUTH_HANDLE_RESPONSE = "+OK Listing of supported mechanisms follows\r\n" +
"PLAIN\r\n" +
"CRAM-MD5\r\n" +
"EXTERNAL\r\n" +
".\r\n";
private static final String AUTH_NO_AUTH_PLAIN_RESPONSE = "+OK Listing of supported mechanisms follows\r\n" +
"CRAM-MD5\r\n" +
"EXTERNAL\r\n" +
".\r\n";
private static final String CAPA = "CAPA\r\n";
private static final String CAPA_RESPONSE = "+OK Listing of supported mechanisms follows\r\n" +
"PLAIN\r\n" +
"CRAM-MD5\r\n" +
"EXTERNAL\r\n" +
".\r\n";
private static final String CAPA_NO_AUTH_PLAIN_RESPONSE = "+OK Listing of supported mechanisms follows\r\n" +
"CRAM-MD5\r\n" +
"EXTERNAL\r\n" +
".\r\n";
private static final String LOGIN_AUTHENTICATED_RESPONSE = "+OK\r\n" + "+OK\r\n";
private static final String LOGIN = "USER "+username+"\r\n" + "PASS "+password+"\r\n";
private static final String AUTH_PLAIN_WITH_LOGIN = "AUTH PLAIN\r\n" +
new String(Base64.encodeBase64(("\000"+username+"\000"+password).getBytes())) + "\r\n";
private static final String AUTH_PLAIN_AUTHENTICATED_RESPONSE = "+OK\r\n" + "+OK\r\n";
private static final String AUTH_PLAIN_FAILED_RESPONSE = "+OK\r\n" + "Plain authentication failure";
private static final String STAT = "STAT\r\n";
private static final String STAT_RESPONSE = "+OK 20 0\r\n";
private static final String UIDL_UNSUPPORTED_RESPONSE = "-ERR UIDL unsupported\r\n";
private static final String UIDL_SUPPORTED_RESPONSE = "+OK UIDL supported\r\n";
private TrustedSocketFactory mockTrustedSocketFactory;
private Socket mockSocket;
private ByteArrayOutputStream outputStream;
private SimplePop3Settings settings;
private TrustedSocketFactory socketFactory;
@Before
public void before() throws Exception {
settings = new SimplePop3Settings();
settings.setUsername(username);
settings.setPassword(password);
socketFactory = TestTrustedSocketFactory.newInstance();
mockTrustedSocketFactory = mock(TrustedSocketFactory.class); //TODO: Remove
mockSocket = mock(Socket.class); //TODO: Remove
outputStream = new ByteArrayOutputStream(); //TODO: Remove
when(mockTrustedSocketFactory.createSocket(null, "server", 12345, null)).thenReturn(mockSocket); //TODO: Remove
when(mockSocket.getOutputStream()).thenReturn(outputStream); //TODO: Remove
when(mockSocket.isConnected()).thenReturn(true); //TODO: Remove
}
private void setSettingsForMockSocket() {
settings.setHost(host);
settings.setPort(port);
settings.setConnectionSecurity(ConnectionSecurity.SSL_TLS_REQUIRED);
}
@Test(expected = CertificateValidationException.class)
public void whenTrustedSocketFactoryThrowsSSLCertificateException_throwCertificateValidationException() throws Exception {
when(mockTrustedSocketFactory.createSocket(null, "server", 12345, null)).thenThrow(
new SSLException(new CertificateException()));
setSettingsForMockSocket();
settings.setAuthType(AuthType.PLAIN);
new Pop3Connection(settings, mockTrustedSocketFactory);
}
@Test(expected = MessagingException.class)
public void whenTrustedSocketFactoryThrowsCertificateException_throwMessagingException() throws Exception {
when(mockTrustedSocketFactory.createSocket(null, "server", 12345, null)).thenThrow(
new SSLException(""));
setSettingsForMockSocket();
settings.setAuthType(AuthType.PLAIN);
new Pop3Connection(settings, mockTrustedSocketFactory);
}
@Test(expected = MessagingException.class)
public void whenTrustedSocketFactoryThrowsGeneralSecurityException_throwMessagingException() throws Exception {
when(mockTrustedSocketFactory.createSocket(null, "server", 12345, null)).thenThrow(
new NoSuchAlgorithmException(""));
setSettingsForMockSocket();
settings.setAuthType(AuthType.PLAIN);
new Pop3Connection(settings, mockTrustedSocketFactory);
}
@Test(expected = MessagingException.class)
public void whenTrustedSocketFactoryThrowsIOException_throwMessagingException() throws Exception {
when(mockTrustedSocketFactory.createSocket(null, "server", 12345, null)).thenThrow(
new IOException(""));
setSettingsForMockSocket();
settings.setAuthType(AuthType.PLAIN);
new Pop3Connection(settings, mockTrustedSocketFactory);
}
@Test(expected = MessagingException.class)
public void whenSocketNotConnected_throwsMessagingException() throws Exception {
when(mockSocket.isConnected()).thenReturn(false);
setSettingsForMockSocket();
settings.setAuthType(AuthType.PLAIN);
new Pop3Connection(settings, mockTrustedSocketFactory);
}
@Test
public void withTLS_connectsToSocket() throws Exception {
String response = INITIAL_RESPONSE +
AUTH_HANDLE_RESPONSE +
CAPA_RESPONSE +
AUTH_PLAIN_AUTHENTICATED_RESPONSE;
when(mockSocket.getInputStream()).thenReturn(new ByteArrayInputStream(response.getBytes()));
setSettingsForMockSocket();
settings.setAuthType(AuthType.PLAIN);
new Pop3Connection(settings, mockTrustedSocketFactory);
assertEquals(AUTH +
CAPA +
AUTH_PLAIN_WITH_LOGIN, new String(outputStream.toByteArray()));
}
@Test
public void withAuthTypePlainAndPlainAuthCapability_performsPlainAuth() throws Exception {
settings.setAuthType(AuthType.PLAIN);
MockPop3Server server = new MockPop3Server();
server.output("+OK POP3 server greeting");
server.expect("AUTH");
server.output("+OK Listing of supported mechanisms follows");
server.output("PLAIN");
server.output("CRAM-MD5");
server.output("EXTERNAL");
server.output(".");
server.expect("CAPA");
server.output("+OK Listing of supported mechanisms follows");
server.output("PLAIN");
server.output("CRAM-MD5");
server.output("EXTERNAL");
server.output(".");
server.expect("AUTH PLAIN");
server.output("+OK");
server.expect(new String(Base64.encodeBase64(("\000"+username+"\000"+password).getBytes())));
server.output("+OK");
startServerAndCreateConnection(server);
server.verifyConnectionStillOpen();
server.verifyInteractionCompleted();
}
@Test
public void withAuthTypePlainAndPlainAuthCapabilityAndInvalidPasswordResponse_throwsException() throws Exception {
settings.setAuthType(AuthType.PLAIN);
MockPop3Server server = new MockPop3Server();
server.output("+OK POP3 server greeting");
server.expect("AUTH");
server.output("+OK Listing of supported mechanisms follows");
server.output("PLAIN");
server.output("CRAM-MD5");
server.output("EXTERNAL");
server.output(".");
server.expect("CAPA");
server.output("+OK Listing of supported mechanisms follows");
server.output("PLAIN");
server.output("CRAM-MD5");
server.output("EXTERNAL");
server.output(".");
server.expect("AUTH PLAIN");
server.output("+OK");
server.expect(new String(Base64.encodeBase64(("\000"+username+"\000"+password).getBytes())));
server.output("-ERR");
try {
startServerAndCreateConnection(server);
fail("Expected auth failure");
} catch (AuthenticationFailedException ignored) {}
server.verifyInteractionCompleted();
}
@Test
public void withAuthTypePlainAndNoPlainAuthCapability_performsLogin() throws Exception {
settings.setAuthType(AuthType.PLAIN);
MockPop3Server server = new MockPop3Server();
server.output("+OK POP3 server greeting");
server.expect("AUTH");
server.output("+OK Listing of supported mechanisms follows");
server.output("CRAM-MD5");
server.output("EXTERNAL");
server.output(".");
server.expect("CAPA");
server.output("+OK Listing of supported mechanisms follows");
server.output("CRAM-MD5");
server.output("EXTERNAL");
server.output(".");
server.expect("USER user");
server.output("+OK");
server.expect("PASS password");
server.output("-ERR");
try {
startServerAndCreateConnection(server);
fail("Expected auth failure");
} catch (AuthenticationFailedException ignored) {}
server.verifyInteractionCompleted();
}
@Test
public void withAuthTypePlainAndNoPlainAuthCapabilityAndLoginFailure_throwsException() throws Exception {
settings.setAuthType(AuthType.PLAIN);
MockPop3Server server = new MockPop3Server();
server.output("+OK POP3 server greeting");
server.expect("AUTH");
server.output("+OK Listing of supported mechanisms follows");
server.output("CRAM-MD5");
server.output("EXTERNAL");
server.output(".");
server.expect("CAPA");
server.output("+OK Listing of supported mechanisms follows");
server.output("CRAM-MD5");
server.output("EXTERNAL");
server.output(".");
server.expect("USER user");
server.output("+OK");
server.expect("PASS password");
server.output("+OK");
startServerAndCreateConnection(server);
server.verifyInteractionCompleted();
}
@Test
public void withAuthTypeCramMd5AndCapability_performsCramMd5Auth() throws IOException, MessagingException {
settings.setAuthType(AuthType.CRAM_MD5);
MockPop3Server server = new MockPop3Server();
server.output("+OK POP3 server greeting");
server.expect("AUTH");
server.output("+OK Listing of supported mechanisms follows");
server.output("PLAIN");
server.output("CRAM-MD5");
server.output("EXTERNAL");
server.output(".");
server.expect("CAPA");
server.output("+OK Listing of supported mechanisms follows");
server.output("PLAIN");
server.output("CRAM-MD5");
server.output("EXTERNAL");
server.output(".");
server.expect("AUTH CRAM-MD5");
server.output("+ abcd");
server.expect("dXNlciBhZGFhZTU2Zjk1NzAxZjQwNDQwZjhhMWU2YzY1ZjZmZg==");
server.output("+OK");
startServerAndCreateConnection(server);
server.verifyConnectionStillOpen();
server.verifyInteractionCompleted();
}
@Test
public void withAuthTypeCramMd5AndCapabilityAndCramFailure_throwsException() throws IOException, MessagingException {
settings.setAuthType(AuthType.CRAM_MD5);
MockPop3Server server = new MockPop3Server();
server.output("+OK POP3 server greeting");
server.expect("AUTH");
server.output("+OK Listing of supported mechanisms follows");
server.output("PLAIN");
server.output("CRAM-MD5");
server.output("EXTERNAL");
server.output(".");
server.expect("CAPA");
server.output("+OK Listing of supported mechanisms follows");
server.output("PLAIN");
server.output("CRAM-MD5");
server.output("EXTERNAL");
server.output(".");
server.expect("AUTH CRAM-MD5");
server.output("+ abcd");
server.expect("dXNlciBhZGFhZTU2Zjk1NzAxZjQwNDQwZjhhMWU2YzY1ZjZmZg==");
server.output("-ERR");
try {
startServerAndCreateConnection(server);
fail("Expected auth failure");
} catch (AuthenticationFailedException ignored) {}
server.verifyInteractionCompleted();
}
@Test
public void withAuthTypeCramMd5AndNoCapability_performsApopAuth() throws IOException, MessagingException {
settings.setAuthType(AuthType.CRAM_MD5);
MockPop3Server server = new MockPop3Server();
server.output("+OK abc<a>abcd");
server.expect("AUTH");
server.output("+OK Listing of supported mechanisms follows");
server.output("PLAIN");
server.output("EXTERNAL");
server.output(".");
server.expect("CAPA");
server.output("+OK Listing of supported mechanisms follows");
server.output("PLAIN");
server.output("EXTERNAL");
server.output(".");
server.expect("APOP user c8e8c560e385faaa6367d4145572b8ea");
server.output("+OK");
startServerAndCreateConnection(server);
server.verifyConnectionStillOpen();
server.verifyInteractionCompleted();
}
@Test
public void withAuthTypeCramMd5AndNoCapabilityAndApopFailure_throwsException() throws IOException, MessagingException {
settings.setAuthType(AuthType.CRAM_MD5);
MockPop3Server server = new MockPop3Server();
server.output("+OK abc<a>abcd");
server.expect("AUTH");
server.output("+OK Listing of supported mechanisms follows");
server.output("PLAIN");
server.output("EXTERNAL");
server.output(".");
server.expect("CAPA");
server.output("+OK Listing of supported mechanisms follows");
server.output("PLAIN");
server.output("EXTERNAL");
server.output(".");
server.expect("APOP user c8e8c560e385faaa6367d4145572b8ea");
server.output("-ERR");
try {
startServerAndCreateConnection(server);
fail("Expected auth failure");
} catch (AuthenticationFailedException ignored) {}
server.verifyInteractionCompleted();
}
@Test
public void withAuthTypeExternalAndCapability_performsExternalAuth() throws IOException, MessagingException {
settings.setAuthType(AuthType.EXTERNAL);
MockPop3Server server = new MockPop3Server();
server.output("+OK POP3 server greeting");
server.expect("AUTH");
server.output("+OK Listing of supported mechanisms follows");
server.output("PLAIN");
server.output("CRAM-MD5");
server.output("EXTERNAL");
server.output(".");
server.expect("CAPA");
server.output("+OK Listing of supported mechanisms follows");
server.output("PLAIN");
server.output("CRAM-MD5");
server.output("EXTERNAL");
server.output(".");
server.expect("AUTH EXTERNAL dXNlcg==");
server.output("+OK");
startServerAndCreateConnection(server);
server.verifyConnectionStillOpen();
server.verifyInteractionCompleted();
}
@Test
public void withAuthTypeExternalAndNoCapability_throwsCVE() throws IOException, MessagingException {
settings.setAuthType(AuthType.EXTERNAL);
MockPop3Server server = new MockPop3Server();
server.output("+OK POP3 server greeting");
server.expect("AUTH");
server.output("+OK Listing of supported mechanisms follows");
server.output("PLAIN");
server.output("CRAM-MD5");
server.output(".");
server.expect("CAPA");
server.output("+OK Listing of supported mechanisms follows");
server.output("PLAIN");
server.output("CRAM-MD5");
server.output("EXTERNAL");
server.output(".");
try {
startServerAndCreateConnection(server);
fail("CVE expected");
} catch (CertificateValidationException e) {
assertEquals(Reason.MissingCapability, e.getReason());
}
server.verifyConnectionStillOpen();
server.verifyInteractionCompleted();
}
@Test
public void withAuthTypeExternalAndCapability_withRejection_throwsCVE() throws IOException, MessagingException {
settings.setAuthType(AuthType.EXTERNAL);
MockPop3Server server = new MockPop3Server();
server.output("+OK POP3 server greeting");
server.expect("AUTH");
server.output("+OK Listing of supported mechanisms follows");
server.output("PLAIN");
server.output("CRAM-MD5");
server.output("EXTERNAL");
server.output(".");
server.expect("CAPA");
server.output("+OK Listing of supported mechanisms follows");
server.output("PLAIN");
server.output("CRAM-MD5");
server.output("EXTERNAL");
server.output(".");
server.expect("AUTH EXTERNAL dXNlcg==");
server.output("-ERR Invalid certificate");
try {
startServerAndCreateConnection(server);
fail("CVE expected");
} catch (CertificateValidationException e) {
assertEquals("POP3 client certificate authentication failed: -ERR Invalid certificate", e.getMessage());
}
server.verifyInteractionCompleted();
}
private Pop3Connection startServerAndCreateConnection(MockPop3Server server) throws IOException,
MessagingException {
server.start();
settings.setHost(server.getHost());
settings.setPort(server.getPort());
return createPop3Connection(settings, socketFactory);
}
private Pop3Connection createPop3Connection(Pop3Settings settings, TrustedSocketFactory socketFactory)
throws MessagingException {
return new Pop3Connection(settings, socketFactory);
}
}

View file

@ -0,0 +1,329 @@
package com.fsck.k9.mail.store.pop3;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import com.fsck.k9.mail.FetchProfile;
import com.fsck.k9.mail.FetchProfile.Item;
import com.fsck.k9.mail.Folder;
import com.fsck.k9.mail.Folder.FolderType;
import com.fsck.k9.mail.MessageRetrievalListener;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.internet.BinaryTempFileBody;
import com.fsck.k9.mail.store.StoreConfig;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
public class Pop3FolderTest {
private Pop3Store mockStore;
private Pop3Connection mockConnection;
private StoreConfig mockStoreConfig;
private MessageRetrievalListener<Pop3Message> mockListener;
private Pop3Folder folder;
@Before
public void before() throws MessagingException {
mockStore = mock(Pop3Store.class);
mockConnection = mock(Pop3Connection.class);
mockStoreConfig = mock(StoreConfig.class);
mockListener = mock(MessageRetrievalListener.class);
when(mockStore.getConfig()).thenReturn(mockStoreConfig);
when(mockStoreConfig.getInboxFolderName()).thenReturn("Inbox");
when(mockStore.createConnection()).thenReturn(mockConnection);
when(mockConnection.executeSimpleCommand(Pop3Commands.STAT_COMMAND)).thenReturn("+OK 10 0");
folder = new Pop3Folder(mockStore, "Inbox");
setupTempDirectory();
}
private void setupTempDirectory() {
File tempDirectory = new File("temp");
if (!tempDirectory.exists()) {
assertTrue(tempDirectory.mkdir());
tempDirectory.deleteOnExit();
}
BinaryTempFileBody.setTempDirectory(tempDirectory);
}
@Test
public void create_withHoldsFoldersArgument_shouldDoNothing() throws Exception {
Pop3Folder folder = new Pop3Folder(mockStore, "TestFolder");
boolean result = folder.create(FolderType.HOLDS_FOLDERS);
assertFalse(result);
verifyZeroInteractions(mockConnection);
}
@Test
public void create_withHoldsMessagesArgument_shouldDoNothing() throws Exception {
Pop3Folder folder = new Pop3Folder(mockStore, "TestFolder");
boolean result = folder.create(FolderType.HOLDS_MESSAGES);
assertFalse(result);
verifyZeroInteractions(mockConnection);
}
@Test
public void exists_withInbox_shouldReturnTrue() throws Exception {
boolean result = folder.exists();
assertTrue(result);
}
@Test
public void exists_withNonInboxFolder_shouldReturnFalse() throws Exception {
folder = new Pop3Folder(mockStore, "TestFolder");
boolean result = folder.exists();
assertFalse(result);
}
@Test
public void getUnreadMessageCount_shouldBeMinusOne() throws Exception {
int result = folder.getUnreadMessageCount();
assertEquals(-1, result);
}
@Test
public void getFlaggedMessageCount_shouldBeMinusOne() throws Exception {
int result = folder.getFlaggedMessageCount();
assertEquals(-1, result);
}
@Test(expected = MessagingException.class)
public void open_withoutInboxFolder_shouldThrow() throws Exception {
Pop3Folder folder = new Pop3Folder(mockStore, "TestFolder");
folder.open(Folder.OPEN_MODE_RW);
}
@Test
public void open_withoutInboxFolder_shouldNotTryAndCreateConnection() throws Exception {
Pop3Folder folder = new Pop3Folder(mockStore, "TestFolder");
try {
folder.open(Folder.OPEN_MODE_RW);
} catch (Exception ignored) {}
verify(mockStore, never()).createConnection();
}
@Test(expected = MessagingException.class)
public void open_withInboxFolderWithExceptionCreatingConnection_shouldThrow()
throws MessagingException {
when(mockStore.createConnection()).thenThrow(new MessagingException("Test"));
folder.open(Folder.OPEN_MODE_RW);
}
@Test
public void open_withInboxFolder_shouldSetMessageCountFromStatResponse()
throws MessagingException {
folder.open(Folder.OPEN_MODE_RW);
int messageCount = folder.getMessageCount();
assertEquals(10, messageCount);
}
@Test(expected = MessagingException.class)
public void open_withInboxFolder_whenStatCommandFails_shouldThrow()
throws MessagingException {
when(mockConnection.executeSimpleCommand(Pop3Commands.STAT_COMMAND))
.thenThrow(new MessagingException("Test"));
folder.open(Folder.OPEN_MODE_RW);
}
@Test
public void open_whenAlreadyOpenWithValidConnection_doesNotCreateAnotherConnection()
throws MessagingException {
folder.open(Folder.OPEN_MODE_RW);
when(mockConnection.isOpen()).thenReturn(true);
folder.open(Folder.OPEN_MODE_RW);
verify(mockStore, times(1)).createConnection();
}
@Test
public void getMode_withFolderOpenedInRO_isRW() throws MessagingException {
folder.open(Folder.OPEN_MODE_RO);
int mode = folder.getMode();
assertEquals(Folder.OPEN_MODE_RW, mode);
}
@Test
public void close_onNonOpenedFolder_succeeds()
throws MessagingException {
folder.close();
}
@Test
public void close_onOpenedFolder_succeeds()
throws MessagingException {
folder.open(Folder.OPEN_MODE_RW);
folder.close();
}
@Test
public void close_onOpenedFolder_sendsQUIT()
throws MessagingException {
folder.open(Folder.OPEN_MODE_RW);
when(mockConnection.isOpen()).thenReturn(true);
folder.close();
verify(mockConnection).executeSimpleCommand(Pop3Commands.QUIT_COMMAND);
}
@Test
public void close_withExceptionQuiting_ignoresException()
throws MessagingException {
folder.open(Folder.OPEN_MODE_RW);
when(mockConnection.isOpen()).thenReturn(true);
doThrow(new MessagingException("Test"))
.when(mockConnection)
.executeSimpleCommand(Pop3Commands.QUIT_COMMAND);
folder.close();
}
@Test
public void close_onOpenedFolder_closesConnection()
throws MessagingException {
folder.open(Folder.OPEN_MODE_RW);
when(mockConnection.isOpen()).thenReturn(true);
folder.close();
verify(mockConnection).close();
}
@Test
public void getMessages_returnsListOfMessagesOnServer() throws IOException, MessagingException {
folder.open(Folder.OPEN_MODE_RW);
when(mockConnection.readLine()).thenReturn("1 abcd").thenReturn(".");
List<Pop3Message> result = folder.getMessages(1, 1, null, mockListener);
assertEquals(1, result.size());
}
@Test(expected = MessagingException.class)
public void getMessages_withInvalidSet_throwsException() throws IOException, MessagingException {
folder.open(Folder.OPEN_MODE_RW);
folder.getMessages(2, 1, null, mockListener);
}
@Test(expected = MessagingException.class)
public void getMessages_withIOExceptionReadingLine_throwsException() throws IOException, MessagingException {
folder.open(Folder.OPEN_MODE_RW);
when(mockConnection.readLine()).thenThrow(new IOException("Test"));
folder.getMessages(1, 1, null, mockListener);
}
@Test
public void getMessage_withPreviouslyFetchedMessage_returnsMessage()
throws IOException, MessagingException {
folder.open(Folder.OPEN_MODE_RW);
List<Pop3Message> messageList = setupMessageFromServer();
Pop3Message message = folder.getMessage("abcd");
assertSame(messageList.get(0), message);
}
@Test
public void getMessage_withNoPreviouslyFetchedMessage_returnsNewMessage()
throws IOException, MessagingException {
folder.open(Folder.OPEN_MODE_RW);
Pop3Message message = folder.getMessage("abcd");
assertNotNull(message);
}
@Test
public void fetch_withEnvelopeProfile_setsSizeOfMessage() throws MessagingException, IOException {
folder.open(Folder.OPEN_MODE_RW);
List<Pop3Message> messageList = setupMessageFromServer();
FetchProfile fetchProfile = new FetchProfile();
fetchProfile.add(Item.ENVELOPE);
when(mockConnection.readLine()).thenReturn("1 100").thenReturn(".");
folder.fetch(messageList, fetchProfile, mockListener);
assertEquals(100, messageList.get(0).getSize());
}
@Test
public void fetch_withBodyProfile_setsContentOfMessage() throws MessagingException, IOException {
InputStream messageInputStream = new ByteArrayInputStream((
"From: <adam@example.org>\r\n" +
"To: <eva@example.org>\r\n" +
"Subject: Testmail\r\n" +
"MIME-Version: 1.0\r\n" +
"Content-type: text/plain\r\n" +
"Content-Transfer-Encoding: 7bit\r\n" +
"\r\n" +
"this is some test text.").getBytes());
folder.open(Folder.OPEN_MODE_RW);
List<Pop3Message> messageList = setupMessageFromServer();
FetchProfile fetchProfile = new FetchProfile();
fetchProfile.add(Item.BODY);
when(mockConnection.readLine()).thenReturn("1 100").thenReturn(".");
when(mockConnection.getInputStream()).thenReturn(messageInputStream);
folder.fetch(messageList, fetchProfile, mockListener);
ByteArrayOutputStream bodyData = new ByteArrayOutputStream();
messageList.get(0).getBody().writeTo(bodyData);
assertEquals("this is some test text.", new String(bodyData.toByteArray(), "UTF-8"));
}
private List<Pop3Message> setupMessageFromServer() throws IOException, MessagingException {
when(mockConnection.readLine()).thenReturn("1 abcd").thenReturn(".");
return folder.getMessages(1, 1, null, mockListener);
}
}

View file

@ -0,0 +1,21 @@
package com.fsck.k9.mail.store.pop3;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.MessagingException;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
public class Pop3MessageTest {
@Test
public void delete_setsDeletedFlag() throws MessagingException {
Pop3Message message = new Pop3Message("001", mock(Pop3Folder.class));
message.delete("Trash");
assertTrue(message.getFlags().contains(Flag.DELETED));
}
}

View file

@ -3,14 +3,18 @@ package com.fsck.k9.mail.store.pop3;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.List;
import com.fsck.k9.mail.AuthType;
import com.fsck.k9.mail.AuthenticationFailedException;
import com.fsck.k9.mail.ConnectionSecurity;
import com.fsck.k9.mail.Folder;
import com.fsck.k9.mail.Folder.FolderType;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.ServerSettings;
import com.fsck.k9.mail.ServerSettings.Type;
import com.fsck.k9.mail.filter.Base64;
import com.fsck.k9.mail.ssl.TrustedSocketFactory;
import com.fsck.k9.mail.store.StoreConfig;
@ -20,9 +24,10 @@ import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
@ -46,6 +51,8 @@ public class Pop3StoreTest {
private static final String AUTH_PLAIN_FAILED_RESPONSE = "+OK\r\n" + "Plain authentication failure";
private static final String STAT = "STAT\r\n";
private static final String STAT_RESPONSE = "+OK 20 0\r\n";
private static final String UIDL_UNSUPPORTED_RESPONSE = "-ERR UIDL unsupported\r\n";
private static final String UIDL_SUPPORTED_RESPONSE = "+OK UIDL supported\r\n";
private Pop3Store store;
@ -68,80 +75,116 @@ public class Pop3StoreTest {
store = new Pop3Store(mockStoreConfig, mockTrustedSocketFactory);
}
@Test
public void decodeUri_withTLSUri_shouldUseStartTls() {
ServerSettings settings = Pop3Store.decodeUri("pop3+tls+://PLAIN:user:password@server:12345");
assertEquals(settings.connectionSecurity, ConnectionSecurity.STARTTLS_REQUIRED);
}
@Test
public void decodeUri_withPlainUri_shouldUseNoSecurity() {
ServerSettings settings = Pop3Store.decodeUri("pop3://PLAIN:user:password@server:12345");
assertEquals(settings.connectionSecurity, ConnectionSecurity.NONE);
}
@Test
public void decodeUri_withExternalCertificateShouldProvideAlias_shouldUseNoSecurity() {
ServerSettings settings = Pop3Store.decodeUri("pop3://EXTERNAL:user:clientCert@server:12345");
assertEquals(settings.clientCertificateAlias, "clientCert");
}
@Test(expected = IllegalArgumentException.class)
public void decodeUri_withNonPop3Uri_shouldThrowException() {
Pop3Store.decodeUri("imap://PLAIN:user:password@server:12345");
}
@Test
public void createUri_withSSLTLS_required_shouldProduceSSLUri() {
ServerSettings settings = new ServerSettings(Type.POP3, "server", 12345, ConnectionSecurity.SSL_TLS_REQUIRED,
AuthType.PLAIN, "user", "password", null);
String uri = Pop3Store.createUri(settings);
assertEquals(uri, "pop3+ssl+://PLAIN:user:password@server:12345");
}
@Test
public void createUri_withSTARTTLSRequired_shouldProduceTLSUri() {
ServerSettings settings = new ServerSettings(Type.POP3, "server", 12345, ConnectionSecurity.STARTTLS_REQUIRED,
AuthType.PLAIN, "user", "password", null);
String uri = Pop3Store.createUri(settings);
assertEquals(uri, "pop3+tls+://PLAIN:user:password@server:12345");
}
@Test
public void createUri_withNONE_shouldProducePop3Uri() {
ServerSettings settings = new ServerSettings(Type.POP3, "server", 12345, ConnectionSecurity.NONE,
AuthType.PLAIN, "user", "password", null);
String uri = Pop3Store.createUri(settings);
assertEquals(uri, "pop3://PLAIN:user:password@server:12345");
}
@Test
public void createUri_withPLAIN_shouldProducePlainAuthUri() {
ServerSettings settings = new ServerSettings(Type.POP3, "server", 12345, ConnectionSecurity.NONE,
AuthType.PLAIN, "user", "password", null);
String uri = Pop3Store.createUri(settings);
assertEquals(uri, "pop3://PLAIN:user:password@server:12345");
}
@Test
public void createUri_withEXTERNAL_shouldProduceExternalAuthUri() {
ServerSettings settings = new ServerSettings(Type.POP3, "server", 12345, ConnectionSecurity.NONE,
AuthType.EXTERNAL, "user", "password", "clientCert");
String uri = Pop3Store.createUri(settings);
assertEquals(uri, "pop3://EXTERNAL:user:clientCert@server:12345");
}
@Test
public void createUri_withCRAMMD5_shouldProduceCRAMMD5AuthUri() {
ServerSettings settings = new ServerSettings(Type.POP3, "server", 12345, ConnectionSecurity.NONE,
AuthType.CRAM_MD5, "user", "password", "clientCert");
String uri = Pop3Store.createUri(settings);
assertEquals(uri, "pop3://CRAM_MD5:user:password@server:12345");
}
@Test(expected = MessagingException.class)
public void withInvalidStoreUri_shouldThrowMessagingException() throws MessagingException {
when(mockStoreConfig.getStoreUri()).thenReturn("pop3://CRAM_MD5:user:password@[]:12345");
store = new Pop3Store(mockStoreConfig, mockTrustedSocketFactory);
}
@Test
public void getFolder_shouldReturnSameFolderEachTime() {
Folder folderOne = store.getFolder("TestFolder");
Folder folderTwo = store.getFolder("TestFolder");
Pop3Folder folderOne = store.getFolder("TestFolder");
Pop3Folder folderTwo = store.getFolder("TestFolder");
assertSame(folderOne, folderTwo);
}
@Test
public void getFolder_shouldReturnFolderWithCorrectName() throws Exception {
Folder folder = store.getFolder("TestFolder");
Pop3Folder folder = store.getFolder("TestFolder");
assertEquals("TestFolder", folder.getName());
}
@Test
public void create_withHoldsFoldersArgument_shouldDoNothing() throws Exception {
Folder folder = store.getFolder("TestFolder");
boolean result = folder.create(FolderType.HOLDS_FOLDERS);
assertFalse(result);
verifyZeroInteractions(mockSocket);
}
@Test
public void create_withHoldsMessagesArgument_shouldDoNothing() throws Exception {
Folder folder = store.getFolder("TestFolder");
boolean result = folder.create(FolderType.HOLDS_MESSAGES);
assertFalse(result);
verifyZeroInteractions(mockSocket);
}
@Test
public void exists_withInbox_shouldReturnTrue() throws Exception {
Folder inbox = store.getFolder("Inbox");
boolean result = inbox.exists();
assertTrue(result);
}
@Test
public void exists_withNonInboxFolder_shouldReturnFalse() throws Exception {
Folder folder = store.getFolder("TestFolder");
boolean result = folder.exists();
assertFalse(result);
}
@Test
public void getUnreadMessageCount_shouldBeMinusOne() throws Exception {
Folder inbox = store.getFolder("Inbox");
int result = inbox.getUnreadMessageCount();
assertEquals(-1, result);
}
@Test
public void getFlaggedMessageCount_shouldBeMinusOne() throws Exception {
Folder inbox = store.getFolder("Inbox");
int result = inbox.getFlaggedMessageCount();
assertEquals(-1, result);
}
@Test
public void getPersonalNamespace_shouldReturnListConsistingOfInbox() throws Exception {
List<? extends Folder> folders = store.getPersonalNamespaces(true);
List<Pop3Folder> folders = store.getPersonalNamespaces(true);
assertEquals(1, folders.size());
assertEquals("Inbox", folders.get(0).getName());
@ -155,12 +198,45 @@ public class Pop3StoreTest {
}
@Test(expected = MessagingException.class)
public void open_withoutInboxFolder_shouldThrow() throws Exception {
Folder folder = store.getFolder("TestFolder");
folder.open(Folder.OPEN_MODE_RW);
public void checkSetting_whenConnectionThrowsException_shouldThrowMessagingException()
throws Exception {
when(mockTrustedSocketFactory.createSocket(any(Socket.class),
anyString(), anyInt(), anyString())).thenThrow(new IOException("Test"));
store.checkSettings();
}
@Test(expected = MessagingException.class)
public void checkSetting_whenUidlUnsupported_shouldThrowMessagingException()
throws Exception {
String response = INITIAL_RESPONSE +
AUTH_HANDLE_RESPONSE +
CAPA_RESPONSE +
AUTH_PLAIN_AUTHENTICATED_RESPONSE +
STAT_RESPONSE +
UIDL_UNSUPPORTED_RESPONSE;
when(mockSocket.getInputStream()).thenReturn(new ByteArrayInputStream(response.getBytes("UTF-8")));
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
when(mockSocket.getOutputStream()).thenReturn(byteArrayOutputStream);
store.checkSettings();
}
@Test
public void checkSetting_whenUidlSupported_shouldReturn()
throws Exception {
String response = INITIAL_RESPONSE +
AUTH_HANDLE_RESPONSE +
CAPA_RESPONSE +
AUTH_PLAIN_AUTHENTICATED_RESPONSE +
STAT_RESPONSE +
UIDL_SUPPORTED_RESPONSE;
when(mockSocket.getInputStream()).thenReturn(new ByteArrayInputStream(response.getBytes("UTF-8")));
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
when(mockSocket.getOutputStream()).thenReturn(byteArrayOutputStream);
store.checkSettings();
}
// Component Level Tests
@Test
public void open_withAuthResponseUsingAuthPlain_shouldRetrieveMessageCountOnAuthenticatedSocket() throws Exception {
String response = INITIAL_RESPONSE +
@ -171,7 +247,7 @@ public class Pop3StoreTest {
when(mockSocket.getInputStream()).thenReturn(new ByteArrayInputStream(response.getBytes("UTF-8")));
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
when(mockSocket.getOutputStream()).thenReturn(byteArrayOutputStream);
Folder folder = store.getFolder("Inbox");
Pop3Folder folder = store.getFolder("Inbox");
folder.open(Folder.OPEN_MODE_RW);
@ -186,7 +262,7 @@ public class Pop3StoreTest {
CAPA_RESPONSE +
AUTH_PLAIN_FAILED_RESPONSE;
when(mockSocket.getInputStream()).thenReturn(new ByteArrayInputStream(response.getBytes("UTF-8")));
Folder folder = store.getFolder("Inbox");
Pop3Folder folder = store.getFolder("Inbox");
folder.open(Folder.OPEN_MODE_RW);
}

View file

@ -0,0 +1,72 @@
package com.fsck.k9.mail.store.pop3;
import com.fsck.k9.mail.AuthType;
import com.fsck.k9.mail.ConnectionSecurity;
class SimplePop3Settings implements Pop3Settings {
private String host;
private int port;
private ConnectionSecurity connectionSecurity = ConnectionSecurity.NONE;
private AuthType authType;
private String username;
private String password;
@Override
public String getHost() {
return host;
}
@Override
public int getPort() {
return port;
}
@Override
public ConnectionSecurity getConnectionSecurity() {
return connectionSecurity;
}
@Override
public AuthType getAuthType() {
return authType;
}
@Override
public String getUsername() {
return username;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getClientCertificateAlias() {
return null;
}
void setHost(String host) {
this.host = host;
}
void setPort(int port) {
this.port = port;
}
void setConnectionSecurity(ConnectionSecurity connectionSecurity) {
this.connectionSecurity = connectionSecurity;
}
void setAuthType(AuthType authType) {
this.authType = authType;
}
void setUsername(String username) {
this.username = username;
}
void setPassword(String password) {
this.password = password;
}
}