Test sending of pending messages

This commit is contained in:
Philip Whitehouse 2017-02-13 23:52:31 +00:00
parent 8ee9b2c591
commit 2e01043a28
5 changed files with 224 additions and 52 deletions

View file

@ -15,24 +15,12 @@ import java.net.URLDecoder;
import java.net.URLEncoder;
public abstract class Transport {
protected static final int SOCKET_CONNECT_TIMEOUT = 10000;
// RFC 1047
protected static final int SOCKET_READ_TIMEOUT = 300000;
public static synchronized Transport getInstance(Context context, StoreConfig storeConfig)
throws MessagingException {
String uri = storeConfig.getTransportUri();
if (uri.startsWith("smtp")) {
OAuth2TokenProvider oauth2TokenProvider = null;
return new SmtpTransport(storeConfig, new DefaultTrustedSocketFactory(context), oauth2TokenProvider);
} else if (uri.startsWith("webdav")) {
return new WebDavTransport(storeConfig);
} else {
throw new MessagingException("Unable to locate an applicable Transport for " + uri);
}
}
/**
* Decodes the contents of transport-specific URIs and puts them into a {@link ServerSettings}
* object.

View file

@ -0,0 +1,32 @@
package com.fsck.k9.mail;
import android.content.Context;
import com.fsck.k9.mail.oauth.OAuth2TokenProvider;
import com.fsck.k9.mail.ssl.DefaultTrustedSocketFactory;
import com.fsck.k9.mail.store.StoreConfig;
import com.fsck.k9.mail.transport.SmtpTransport;
import com.fsck.k9.mail.transport.WebDavTransport;
public class TransportProvider {
private static TransportProvider transportProvider = new TransportProvider();
public static TransportProvider getInstance() {
return transportProvider;
}
public synchronized Transport getInstance(Context context, StoreConfig storeConfig)
throws MessagingException {
String uri = storeConfig.getTransportUri();
if (uri.startsWith("smtp")) {
OAuth2TokenProvider oauth2TokenProvider = null;
return new SmtpTransport(storeConfig, new DefaultTrustedSocketFactory(context),
oauth2TokenProvider);
} else if (uri.startsWith("webdav")) {
return new WebDavTransport(storeConfig);
} else {
throw new MessagingException("Unable to locate an applicable Transport for " + uri);
}
}
}

View file

@ -27,6 +27,7 @@ import com.fsck.k9.mail.CertificateValidationException;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Store;
import com.fsck.k9.mail.Transport;
import com.fsck.k9.mail.TransportProvider;
import com.fsck.k9.mail.store.webdav.WebDavStore;
import com.fsck.k9.mail.filter.Hex;
import java.security.cert.CertificateException;
@ -475,7 +476,7 @@ public class AccountSetupCheckSettings extends K9Activity implements OnClickList
if (!(account.getRemoteStore() instanceof WebDavStore)) {
publishProgress(R.string.account_setup_check_settings_check_outgoing_msg);
}
Transport transport = Transport.getInstance(K9.app, account);
Transport transport = TransportProvider.getInstance().getInstance(K9.app, account);
transport.close();
try {
transport.open();

View file

@ -82,6 +82,7 @@ import com.fsck.k9.mail.PushReceiver;
import com.fsck.k9.mail.Pusher;
import com.fsck.k9.mail.Store;
import com.fsck.k9.mail.Transport;
import com.fsck.k9.mail.TransportProvider;
import com.fsck.k9.mail.internet.MessageExtractor;
import com.fsck.k9.mail.internet.MimeMessage;
import com.fsck.k9.mail.internet.MimeMessageHelper;
@ -140,6 +141,7 @@ public class MessagingController {
private final ConcurrentHashMap<Account, Pusher> pushers = new ConcurrentHashMap<>();
private final ExecutorService threadPool = Executors.newCachedThreadPool();
private final MemorizingMessagingListener memorizingMessagingListener = new MemorizingMessagingListener();
private final TransportProvider transportProvider;
private MessagingListener checkMailListener = null;
@ -151,17 +153,20 @@ public class MessagingController {
Context appContext = context.getApplicationContext();
NotificationController notificationController = NotificationController.newInstance(appContext);
Contacts contacts = Contacts.getInstance(context);
inst = new MessagingController(appContext, notificationController, contacts);
TransportProvider transportProvider = TransportProvider.getInstance();
inst = new MessagingController(appContext, notificationController, contacts, transportProvider);
}
return inst;
}
@VisibleForTesting
MessagingController(Context context, NotificationController notificationController, Contacts contacts) {
MessagingController(Context context, NotificationController notificationController,
Contacts contacts, TransportProvider transportProvider) {
this.context = context;
this.notificationController = notificationController;
this.contacts = contacts;
this.transportProvider = transportProvider;
controllerThread = new Thread(new Runnable() {
@Override
@ -2717,7 +2722,8 @@ public class MessagingController {
/**
* Attempt to send any messages that are sitting in the Outbox.
*/
private void sendPendingMessagesSynchronous(final Account account) {
@VisibleForTesting
protected void sendPendingMessagesSynchronous(final Account account) {
LocalFolder localFolder = null;
Exception lastFailure = null;
boolean wasPermanentFailure = false;
@ -2726,6 +2732,9 @@ public class MessagingController {
localFolder = localStore.getFolder(
account.getOutboxFolderName());
if (!localFolder.exists()) {
if (K9.DEBUG) {
Log.v(K9.LOG_TAG, "Outbox does not exist");
}
return;
}
for (MessagingListener l : getListeners()) {
@ -2748,9 +2757,11 @@ public class MessagingController {
fp.add(FetchProfile.Item.BODY);
if (K9.DEBUG)
Log.i(K9.LOG_TAG, "Scanning folder '" + account.getOutboxFolderName() + "' (" + localFolder.getId() + ") for messages to send");
Log.i(K9.LOG_TAG, "Scanning folder '" + account.getOutboxFolderName()
+ "' (" + localFolder.getId() + ") for messages to send");
Transport transport = transportProvider.getInstance(K9.app, account);
Transport transport = Transport.getInstance(K9.app, account);
for (LocalMessage message : localMessages) {
if (message.isSet(Flag.DELETED)) {
message.destroy();
@ -2762,31 +2773,25 @@ public class MessagingController {
if (oldCount != null) {
count = oldCount;
}
if (K9.DEBUG)
Log.i(K9.LOG_TAG, "Send count for message " + message.getUid() + " is " + count.get());
if (count.incrementAndGet() > K9.MAX_SEND_ATTEMPTS) {
Log.e(K9.LOG_TAG, "Send count for message " + message.getUid() + " can't be delivered after " + K9.MAX_SEND_ATTEMPTS + " attempts. Giving up until the user restarts the device");
Log.e(K9.LOG_TAG, "Send count for message " + message.getUid() + " can't be delivered after "
+ K9.MAX_SEND_ATTEMPTS + " attempts. Giving up until the user restarts the device");
notificationController.showSendFailedNotification(account,
new MessagingException(message.getSubject()));
continue;
}
localFolder.fetch(Collections.singletonList(message), fp, null);
try {
if (message.getHeader(K9.IDENTITY_HEADER).length > 0) {
Log.v(K9.LOG_TAG, "The user has set the Outbox and Drafts folder to the same thing. " +
"This message appears to be a draft, so K-9 will not send it");
continue;
}
message.setFlag(Flag.X_SEND_IN_PROGRESS, true);
if (K9.DEBUG)
Log.i(K9.LOG_TAG, "Sending message with UID " + message.getUid());
@ -2797,24 +2802,7 @@ public class MessagingController {
for (MessagingListener l : getListeners()) {
l.synchronizeMailboxProgress(account, account.getSentFolderName(), progress, todo);
}
if (!account.hasSentFolder()) {
if (K9.DEBUG)
Log.i(K9.LOG_TAG, "Account does not have a sent mail folder; deleting sent message");
message.setFlag(Flag.DELETED, true);
} else {
LocalFolder localSentFolder = localStore.getFolder(account.getSentFolderName());
if (K9.DEBUG)
Log.i(K9.LOG_TAG, "Moving sent message to folder '" + account.getSentFolderName() + "' (" + localSentFolder.getId() + ") ");
localFolder.moveMessages(Collections.singletonList(message), localSentFolder);
if (K9.DEBUG)
Log.i(K9.LOG_TAG, "Moved sent message to folder '" + account.getSentFolderName() + "' (" + localSentFolder.getId() + ") ");
PendingCommand command = PendingAppend.create(localSentFolder.getName(), message.getUid());
queuePendingCommand(account, command);
processPendingCommands(account);
}
moveOrDeleteSentMessage(account, localStore, localFolder, message);
} catch (AuthenticationFailedException e) {
lastFailure = e;
wasPermanentFailure = false;
@ -2841,9 +2829,7 @@ public class MessagingController {
} catch (Exception e) {
lastFailure = e;
wasPermanentFailure = false;
Log.e(K9.LOG_TAG, "Failed to fetch message for sending", e);
addErrorMessage(account, "Failed to fetch message for sending", e);
notifySynchronizeMailboxFailed(account, localFolder, e);
}
@ -2864,6 +2850,9 @@ public class MessagingController {
Log.i(K9.LOG_TAG, "Failed to send pending messages because storage is not available - trying again later.");
throw new UnavailableAccountException(e);
} catch (Exception e) {
if (K9.DEBUG) {
Log.v(K9.LOG_TAG, "Failed to send pending messages", e);
}
for (MessagingListener l : getListeners()) {
l.sendPendingMessagesFailed(account);
}
@ -2877,6 +2866,28 @@ public class MessagingController {
}
}
private void moveOrDeleteSentMessage(Account account, LocalStore localStore,
LocalFolder localFolder, LocalMessage message) throws MessagingException {
if (!account.hasSentFolder()) {
if (K9.DEBUG)
Log.i(K9.LOG_TAG, "Account does not have a sent mail folder; deleting sent message");
message.setFlag(Flag.DELETED, true);
} else {
LocalFolder localSentFolder = localStore.getFolder(account.getSentFolderName());
if (K9.DEBUG)
Log.i(K9.LOG_TAG, "Moving sent message to folder '" + account.getSentFolderName() + "' (" + localSentFolder.getId() + ") ");
localFolder.moveMessages(Collections.singletonList(message), localSentFolder);
if (K9.DEBUG)
Log.i(K9.LOG_TAG, "Moved sent message to folder '" + account.getSentFolderName() + "' (" + localSentFolder.getId() + ") ");
PendingCommand command = PendingAppend.create(localSentFolder.getName(), message.getUid());
queuePendingCommand(account, command);
processPendingCommands(account);
}
}
private void handleSendFailure(Account account, Store localStore, Folder localFolder, Message message,
Exception exception, boolean permanentFailure) throws MessagingException {

View file

@ -17,6 +17,8 @@ import com.fsck.k9.K9;
import com.fsck.k9.K9RobolectricTestRunner;
import com.fsck.k9.Preferences;
import com.fsck.k9.helper.Contacts;
import com.fsck.k9.mail.AuthenticationFailedException;
import com.fsck.k9.mail.CertificateValidationException;
import com.fsck.k9.mail.FetchProfile;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Folder;
@ -24,6 +26,8 @@ import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessageRetrievalListener;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Store;
import com.fsck.k9.mail.Transport;
import com.fsck.k9.mail.TransportProvider;
import com.fsck.k9.mailstore.LocalFolder;
import com.fsck.k9.mailstore.LocalMessage;
import com.fsck.k9.mailstore.LocalStore;
@ -36,12 +40,14 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InOrder;
import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.shadows.ShadowApplication;
import org.robolectric.shadows.ShadowLog;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@ -54,10 +60,12 @@ import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
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;
@ -65,6 +73,7 @@ import static org.mockito.Mockito.when;
@RunWith(K9RobolectricTestRunner.class)
public class MessagingControllerTest {
private static final String FOLDER_NAME = "Folder";
private static final String SENT_FOLDER_NAME = "Sent";
private static final int MAXIMUM_SMALL_MESSAGE_SIZE = 1000;
private static final String MESSAGE_UID1 = "message-uid1";
@ -85,6 +94,8 @@ public class MessagingControllerTest {
@Mock
private LocalFolder errorFolder;
@Mock
private LocalFolder sentFolder;
@Mock
private Folder remoteFolder;
@Mock
private LocalStore localStore;
@ -92,6 +103,10 @@ public class MessagingControllerTest {
private Store remoteStore;
@Mock
private NotificationController notificationController;
@Mock
private TransportProvider transportProvider;
@Mock
private Transport transport;
@Captor
private ArgumentCaptor<List<Message>> messageListCaptor;
@Captor
@ -116,15 +131,18 @@ public class MessagingControllerTest {
private LocalMessage localNewMessage1;
@Mock
private LocalMessage localNewMessage2;
@Mock
private LocalMessage localMessageToSend1;
private volatile boolean hasFetchedMessage = false;
@Before
public void setUp() throws MessagingException {
ShadowLog.stream = System.out;
MockitoAnnotations.initMocks(this);
appContext = ShadowApplication.getInstance().getApplicationContext();
controller = new MessagingController(appContext, notificationController, contacts);
controller = new MessagingController(appContext, notificationController, contacts, transportProvider);
configureAccount();
configureLocalStore();
@ -164,12 +182,14 @@ public class MessagingControllerTest {
}
@Test()
public void clearFolderSynchronous_whenExceptionThrown_shouldAddErrorMessage() throws MessagingException {
doThrow(new RuntimeException("Test")).when(localFolder).open(Folder.OPEN_MODE_RW);
public void clearFolderSynchronous_whenExceptionThrown_shouldAddErrorMessageInDebug() throws MessagingException {
if (K9.DEBUG) {
doThrow(new RuntimeException("Test")).when(localFolder).open(Folder.OPEN_MODE_RW);
controller.clearFolderSynchronous(account, FOLDER_NAME, listener);
controller.clearFolderSynchronous(account, FOLDER_NAME, listener);
verify(errorFolder).appendMessages(any(List.class));
verify(errorFolder).appendMessages(any(List.class));
}
}
@Test()
@ -532,6 +552,112 @@ public class MessagingControllerTest {
verify(listener).remoteSearchFinished(FOLDER_NAME, 0, 50, Collections.<Message>emptyList());
}
@Test
public void sendPendingMessagesSynchronous_withNonExistentOutbox_shouldNotStartSync() throws MessagingException {
when(account.getOutboxFolderName()).thenReturn(FOLDER_NAME);
when(localFolder.exists()).thenReturn(false);
controller.addListener(listener);
controller.sendPendingMessagesSynchronous(account);
verifyZeroInteractions(listener);
}
@Test
public void sendPendingMessagesSynchronous_shouldCallListenerOnStart() throws MessagingException {
setupAccountWithMessageToSend();
controller.sendPendingMessagesSynchronous(account);
verify(listener).sendPendingMessagesStarted(account);
}
@Test
public void sendPendingMessagesSynchronous_shouldSetProgress() throws MessagingException {
setupAccountWithMessageToSend();
controller.sendPendingMessagesSynchronous(account);
verify(listener).synchronizeMailboxProgress(account, "Sent", 0, 1);
}
@Test
public void sendPendingMessagesSynchronous_shouldSendMessageUsingTransport() throws MessagingException {
setupAccountWithMessageToSend();
controller.sendPendingMessagesSynchronous(account);
verify(transport).sendMessage(localMessageToSend1);
}
@Test
public void sendPendingMessagesSynchronous_shouldSetAndRemoveSendInProgressFlag() throws MessagingException {
setupAccountWithMessageToSend();
controller.sendPendingMessagesSynchronous(account);
InOrder ordering = inOrder(localMessageToSend1, transport);
ordering.verify(localMessageToSend1).setFlag(Flag.X_SEND_IN_PROGRESS, true);
ordering.verify(transport).sendMessage(localMessageToSend1);
ordering.verify(localMessageToSend1).setFlag(Flag.X_SEND_IN_PROGRESS, false);
}
@Test
public void sendPendingMessagesSynchronous_shouldMarkSentMessageAsSeen() throws MessagingException {
setupAccountWithMessageToSend();
controller.sendPendingMessagesSynchronous(account);
verify(localMessageToSend1).setFlag(Flag.SEEN, true);
}
@Test
public void sendPendingMessagesSynchronous_whenMessageSentSuccesfully_shouldUpdateProgress() throws MessagingException {
setupAccountWithMessageToSend();
controller.sendPendingMessagesSynchronous(account);
verify(listener).synchronizeMailboxProgress(account, "Sent", 1, 1);
}
@Test
public void sendPendingMessagesSynchronous_shouldUpdateProgress() throws MessagingException {
setupAccountWithMessageToSend();
controller.sendPendingMessagesSynchronous(account);
verify(listener).synchronizeMailboxProgress(account, "Sent", 1, 1);
}
@Test
public void sendPendingMessagesSynchronous_withAuthenticationFailure_shouldNotify() throws MessagingException {
setupAccountWithMessageToSend();
doThrow(new AuthenticationFailedException("Test")).when(transport).sendMessage(localMessageToSend1);
controller.sendPendingMessagesSynchronous(account);
verify(notificationController).showAuthenticationErrorNotification(account, false);
}
@Test
public void sendPendingMessagesSynchronous_withCertificateFailure_shouldNotify() throws MessagingException {
setupAccountWithMessageToSend();
doThrow(new CertificateValidationException("Test")).when(transport).sendMessage(localMessageToSend1);
controller.sendPendingMessagesSynchronous(account);
verify(notificationController).showCertificateErrorNotification(account, false);
}
@Test
public void sendPendingMessagesSynchronous_shouldCallListenerOnCompletion() throws MessagingException {
setupAccountWithMessageToSend();
controller.sendPendingMessagesSynchronous(account);
verify(listener).sendPendingMessagesCompleted(account);
}
@Test
public void synchronizeMailboxSynchronous_withOneMessageInRemoteFolder_shouldFinishWithoutError()
@ -758,6 +884,20 @@ public class MessagingControllerTest {
assertEquals(FetchProfile.Item.BODY_SANE, fetchProfileCaptor.getAllValues().get(3).get(0));
}
private void setupAccountWithMessageToSend() throws MessagingException {
when(account.getOutboxFolderName()).thenReturn(FOLDER_NAME);
when(account.hasSentFolder()).thenReturn(true);
when(account.getSentFolderName()).thenReturn(SENT_FOLDER_NAME);
when(localStore.getFolder(SENT_FOLDER_NAME)).thenReturn(sentFolder);
when(sentFolder.getId()).thenReturn(1L);
when(localFolder.exists()).thenReturn(true);
when(transportProvider.getInstance(appContext, account)).thenReturn(transport);
when(localFolder.getMessages(null)).thenReturn(Collections.singletonList(localMessageToSend1));
when(localMessageToSend1.getUid()).thenReturn("localMessageToSend1");
when(localMessageToSend1.getHeader(K9.IDENTITY_HEADER)).thenReturn(new String[]{});
controller.addListener(listener);
}
private void respondToFetchEnvelopesWithMessage(final Message message) throws MessagingException {
doAnswer(new Answer() {
@Override