diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java b/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java index 4254559d5..72d2830d6 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java @@ -57,7 +57,7 @@ public abstract class Message implements Part, Body { final int MULTIPLIER = 31; int result = 1; - result = MULTIPLIER * result + mFolder.getName().hashCode(); + result = MULTIPLIER * result + (mFolder != null ? mFolder.getName().hashCode() : 0); result = MULTIPLIER * result + mUid.hashCode(); return result; } diff --git a/k9mail/src/main/java/com/fsck/k9/activity/MessageLoaderHelper.java b/k9mail/src/main/java/com/fsck/k9/activity/MessageLoaderHelper.java index 4b02b3e15..70248f61c 100644 --- a/k9mail/src/main/java/com/fsck/k9/activity/MessageLoaderHelper.java +++ b/k9mail/src/main/java/com/fsck/k9/activity/MessageLoaderHelper.java @@ -13,6 +13,8 @@ import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.UiThread; + +import com.fsck.k9.ui.crypto.OpenPgpApiFactory; import timber.log.Timber; import com.fsck.k9.Account; @@ -268,7 +270,7 @@ public class MessageLoaderHelper { messageCryptoHelper = retainCryptoHelperFragment.getData(); } if (messageCryptoHelper == null || messageCryptoHelper.isConfiguredForOutdatedCryptoProvider()) { - messageCryptoHelper = new MessageCryptoHelper(context); + messageCryptoHelper = new MessageCryptoHelper(context, new OpenPgpApiFactory()); retainCryptoHelperFragment.setData(messageCryptoHelper); } messageCryptoHelper.asyncStartOrResumeProcessingMessage( diff --git a/k9mail/src/main/java/com/fsck/k9/crypto/MessageDecryptVerifier.java b/k9mail/src/main/java/com/fsck/k9/crypto/MessageDecryptVerifier.java index 55e14c50c..cec1e0581 100644 --- a/k9mail/src/main/java/com/fsck/k9/crypto/MessageDecryptVerifier.java +++ b/k9mail/src/main/java/com/fsck/k9/crypto/MessageDecryptVerifier.java @@ -200,11 +200,9 @@ public class MessageDecryptVerifier { if (body instanceof Multipart) { Multipart multi = (Multipart) body; BodyPart signatureBody = multi.getBodyPart(1); - if (isSameMimeType(signatureBody.getMimeType(), APPLICATION_PGP_SIGNATURE)) { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - signatureBody.getBody().writeTo(bos); - return bos.toByteArray(); - } + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + signatureBody.getBody().writeTo(bos); + return bos.toByteArray(); } } diff --git a/k9mail/src/main/java/com/fsck/k9/ui/crypto/MessageCryptoHelper.java b/k9mail/src/main/java/com/fsck/k9/ui/crypto/MessageCryptoHelper.java index dcc8c328e..59550fd95 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/crypto/MessageCryptoHelper.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/crypto/MessageCryptoHelper.java @@ -16,7 +16,6 @@ import android.content.Intent; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.WorkerThread; -import timber.log.Timber; import com.fsck.k9.K9; import com.fsck.k9.crypto.MessageDecryptVerifier; @@ -24,6 +23,7 @@ import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Body; import com.fsck.k9.mail.BodyPart; import com.fsck.k9.mail.Flag; +import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Multipart; import com.fsck.k9.mail.Part; @@ -34,7 +34,6 @@ import com.fsck.k9.mail.internet.SizeAware; import com.fsck.k9.mail.internet.TextBody; import com.fsck.k9.mailstore.CryptoResultAnnotation; import com.fsck.k9.mailstore.CryptoResultAnnotation.CryptoError; -import com.fsck.k9.mailstore.LocalMessage; import com.fsck.k9.mailstore.MessageHelper; import com.fsck.k9.mailstore.MimePartStreamParser; import com.fsck.k9.mailstore.util.FileFactory; @@ -52,6 +51,7 @@ import org.openintents.openpgp.util.OpenPgpApi.OpenPgpDataSource; import org.openintents.openpgp.util.OpenPgpServiceConnection; import org.openintents.openpgp.util.OpenPgpServiceConnection.OnBound; import org.openintents.openpgp.util.OpenPgpUtils; +import timber.log.Timber; public class MessageCryptoHelper { @@ -68,7 +68,7 @@ public class MessageCryptoHelper { @Nullable private MessageCryptoCallback callback; - private LocalMessage currentMessage; + private Message currentMessage; private OpenPgpDecryptionResult cachedDecryptionResult; private MessageCryptoAnnotations queuedResult; private PendingIntent queuedPendingIntent; @@ -84,15 +84,17 @@ public class MessageCryptoHelper { private OpenPgpApi openPgpApi; private OpenPgpServiceConnection openPgpServiceConnection; + private OpenPgpApiFactory openPgpApiFactory; - public MessageCryptoHelper(Context context) { + public MessageCryptoHelper(Context context, OpenPgpApiFactory openPgpApiFactory) { this.context = context.getApplicationContext(); if (!K9.isOpenPgpProviderConfigured()) { throw new IllegalStateException("MessageCryptoHelper must only be called with a openpgp provider!"); } + this.openPgpApiFactory = openPgpApiFactory; openPgpProviderPackage = K9.getOpenPgpProvider(); } @@ -100,7 +102,7 @@ public class MessageCryptoHelper { return !openPgpProviderPackage.equals(K9.getOpenPgpProvider()); } - public void asyncStartOrResumeProcessingMessage(LocalMessage message, MessageCryptoCallback callback, + public void asyncStartOrResumeProcessingMessage(Message message, MessageCryptoCallback callback, OpenPgpDecryptionResult cachedDecryptionResult) { if (this.currentMessage != null) { reattachCallback(message, callback); @@ -215,9 +217,10 @@ public class MessageCryptoHelper { private void connectToCryptoProviderService() { openPgpServiceConnection = new OpenPgpServiceConnection(context, openPgpProviderPackage, new OnBound() { + @Override public void onBound(IOpenPgpService2 service) { - openPgpApi = new OpenPgpApi(context, service); + openPgpApi = openPgpApiFactory.createOpenPgpApi(context, service); decryptOrVerifyNextPart(); } @@ -621,7 +624,7 @@ public class MessageCryptoHelper { } } - private void reattachCallback(LocalMessage message, MessageCryptoCallback callback) { + private void reattachCallback(Message message, MessageCryptoCallback callback) { if (!message.equals(currentMessage)) { throw new AssertionError("Callback may only be reattached for the same message!"); } @@ -726,5 +729,4 @@ public class MessageCryptoHelper { return NO_REPLACEMENT_PART; } } - } diff --git a/k9mail/src/main/java/com/fsck/k9/ui/crypto/OpenPgpApiFactory.java b/k9mail/src/main/java/com/fsck/k9/ui/crypto/OpenPgpApiFactory.java new file mode 100644 index 000000000..72a20b63b --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/ui/crypto/OpenPgpApiFactory.java @@ -0,0 +1,14 @@ +package com.fsck.k9.ui.crypto; + + +import android.content.Context; + +import org.openintents.openpgp.util.OpenPgpApi; +import org.openintents.openpgp.IOpenPgpService2; + + +public class OpenPgpApiFactory { + OpenPgpApi createOpenPgpApi(Context context, IOpenPgpService2 service) { + return new OpenPgpApi(context, service); + } +} diff --git a/k9mail/src/test/java/com/fsck/k9/ui/crypto/MessageCryptoHelperTest.java b/k9mail/src/test/java/com/fsck/k9/ui/crypto/MessageCryptoHelperTest.java new file mode 100644 index 000000000..8915b62e3 --- /dev/null +++ b/k9mail/src/test/java/com/fsck/k9/ui/crypto/MessageCryptoHelperTest.java @@ -0,0 +1,259 @@ +package com.fsck.k9.ui.crypto; + + +import java.io.OutputStream; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; + +import com.fsck.k9.K9; +import com.fsck.k9.mail.Address; +import com.fsck.k9.mail.Body; +import com.fsck.k9.mail.BodyPart; +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.Multipart; +import com.fsck.k9.mail.internet.MimeBodyPart; +import com.fsck.k9.mail.internet.MimeMessage; +import com.fsck.k9.mail.internet.MimeMultipart; +import com.fsck.k9.mail.internet.TextBody; +import com.fsck.k9.mailstore.CryptoResultAnnotation; +import com.fsck.k9.mailstore.CryptoResultAnnotation.CryptoError; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.openintents.openpgp.IOpenPgpService2; +import org.openintents.openpgp.OpenPgpDecryptionResult; +import org.openintents.openpgp.OpenPgpSignatureResult; +import org.openintents.openpgp.util.OpenPgpApi; +import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpSinkResultCallback; +import org.openintents.openpgp.util.OpenPgpApi.OpenPgpDataSink; +import org.openintents.openpgp.util.OpenPgpApi.OpenPgpDataSource; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertSame; +import static junit.framework.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + + +@SuppressWarnings("unchecked") +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE, sdk = 21) +public class MessageCryptoHelperTest { + private MessageCryptoHelper messageCryptoHelper; + private OpenPgpApi openPgpApi; + private Intent capturedApiIntent; + private IOpenPgpSinkResultCallback capturedCallback; + private MessageCryptoCallback messageCryptoCallback; + + + @Before + public void setUp() throws Exception { + openPgpApi = mock(OpenPgpApi.class); + + K9.setOpenPgpProvider("org.example.dummy"); + + OpenPgpApiFactory openPgpApiFactory = mock(OpenPgpApiFactory.class); + when(openPgpApiFactory.createOpenPgpApi(any(Context.class), any(IOpenPgpService2.class))).thenReturn(openPgpApi); + messageCryptoHelper = new MessageCryptoHelper(RuntimeEnvironment.application, openPgpApiFactory); + messageCryptoCallback = mock(MessageCryptoCallback.class); + } + + @Test + public void textPlain() throws Exception { + MimeMessage message = new MimeMessage(); + message.setUid("msguid"); + message.setHeader("Content-Type", "text/plain"); + + MessageCryptoCallback messageCryptoCallback = mock(MessageCryptoCallback.class); + messageCryptoHelper.asyncStartOrResumeProcessingMessage(message, messageCryptoCallback, null); + + ArgumentCaptor captor = ArgumentCaptor.forClass(MessageCryptoAnnotations.class); + verify(messageCryptoCallback).onCryptoOperationsFinished(captor.capture()); + MessageCryptoAnnotations annotations = captor.getValue(); + assertTrue(annotations.isEmpty()); + verifyNoMoreInteractions(messageCryptoCallback); + } + + @Test + public void multipartSigned__withNullBody__shouldReturnSignedIncomplete() throws Exception { + MimeMessage message = new MimeMessage(); + message.setUid("msguid"); + message.setHeader("Content-Type", "multipart/signed"); + + MessageCryptoCallback messageCryptoCallback = mock(MessageCryptoCallback.class); + messageCryptoHelper.asyncStartOrResumeProcessingMessage(message, messageCryptoCallback, null); + + assertPartAnnotationHasState(message, messageCryptoCallback, CryptoError.OPENPGP_SIGNED_BUT_INCOMPLETE, null, + null, null, null); + } + + @Test + public void multipartEncrypted__withNullBody__shouldReturnEncryptedIncomplete() throws Exception { + MimeMessage message = new MimeMessage(); + message.setUid("msguid"); + message.setHeader("Content-Type", "multipart/encrypted"); + + MessageCryptoCallback messageCryptoCallback = mock(MessageCryptoCallback.class); + messageCryptoHelper.asyncStartOrResumeProcessingMessage(message, messageCryptoCallback, null); + + assertPartAnnotationHasState( + message, messageCryptoCallback, CryptoError.OPENPGP_ENCRYPTED_BUT_INCOMPLETE, null, null, null, null); + } + + @Test + public void multipartEncrypted__withUnknownProtocol__shouldReturnEncryptedUnsupported() throws Exception { + MimeMessage message = new MimeMessage(); + message.setUid("msguid"); + message.setHeader("Content-Type", "multipart/encrypted; protocol=\"unknown protocol\""); + message.setBody(new MimeMultipart("multipart/encrypted", "--------")); + + MessageCryptoCallback messageCryptoCallback = mock(MessageCryptoCallback.class); + messageCryptoHelper.asyncStartOrResumeProcessingMessage(message, messageCryptoCallback, null); + + assertPartAnnotationHasState(message, messageCryptoCallback, CryptoError.ENCRYPTED_BUT_UNSUPPORTED, null, null, + null, null); + } + + @Test + public void multipartSigned__withUnknownProtocol__shouldReturnSignedUnsupported() throws Exception { + MimeMessage message = new MimeMessage(); + message.setUid("msguid"); + message.setHeader("Content-Type", "multipart/signed; protocol=\"unknown protocol\""); + message.setBody(new MimeMultipart("multipart/encrypted", "--------")); + + MessageCryptoCallback messageCryptoCallback = mock(MessageCryptoCallback.class); + messageCryptoHelper.asyncStartOrResumeProcessingMessage(message, messageCryptoCallback, null); + + assertPartAnnotationHasState(message, messageCryptoCallback, CryptoError.SIGNED_BUT_UNSUPPORTED, null, null, + null, null); + } + + @Test + public void multipartSigned__shouldCallOpenPgpApiAsync() throws Exception { + BodyPart signedBodyPart = spy(new MimeBodyPart(new TextBody("text"))); + BodyPart signatureBodyPart = new MimeBodyPart(new TextBody("text")); + + Multipart messageBody = new MimeMultipart("boundary1"); + messageBody.addBodyPart(signedBodyPart); + messageBody.addBodyPart(signatureBodyPart); + + MimeMessage message = new MimeMessage(); + message.setUid("msguid"); + message.setHeader("Content-Type", "multipart/signed; protocol=\"application/pgp-signature\""); + message.setFrom(Address.parse("Test ")[0]); + message.setBody(messageBody); + + OutputStream outputStream = mock(OutputStream.class); + + + processSignedMessageAndCaptureMocks(message, signedBodyPart, outputStream); + + + assertEquals(OpenPgpApi.ACTION_DECRYPT_VERIFY, capturedApiIntent.getAction()); + assertEquals("test@example.org", capturedApiIntent.getStringExtra(OpenPgpApi.EXTRA_SENDER_ADDRESS)); + } + + @Test + public void multipartEncrypted__shouldCallOpenPgpApiAsync() throws Exception { + BodyPart dummyBodyPart = new MimeBodyPart(new TextBody("text")); + Body encryptedBody = spy(new TextBody("encrypted data")); + BodyPart encryptedBodyPart = spy(new MimeBodyPart(encryptedBody)); + + Multipart messageBody = new MimeMultipart("boundary1"); + messageBody.addBodyPart(dummyBodyPart); + messageBody.addBodyPart(encryptedBodyPart); + + MimeMessage message = new MimeMessage(); + message.setUid("msguid"); + message.setHeader("Content-Type", "multipart/encrypted; protocol=\"application/pgp-encrypted\""); + message.setFrom(Address.parse("Test ")[0]); + message.setBody(messageBody); + + OutputStream outputStream = mock(OutputStream.class); + + Intent resultIntent = new Intent(); + resultIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); + OpenPgpDecryptionResult decryptionResult = mock(OpenPgpDecryptionResult.class); + resultIntent.putExtra(OpenPgpApi.RESULT_DECRYPTION, decryptionResult); + OpenPgpSignatureResult signatureResult = mock(OpenPgpSignatureResult.class); + resultIntent.putExtra(OpenPgpApi.RESULT_SIGNATURE, signatureResult); + PendingIntent pendingIntent = mock(PendingIntent.class); + resultIntent.putExtra(OpenPgpApi.RESULT_INTENT, pendingIntent); + + + processEncryptedMessageAndCaptureMocks(message, encryptedBody, outputStream); + + MimeBodyPart decryptedPart = new MimeBodyPart(new TextBody("text")); + capturedCallback.onReturn(resultIntent, decryptedPart); + + + assertEquals(OpenPgpApi.ACTION_DECRYPT_VERIFY, capturedApiIntent.getAction()); + assertEquals("test@example.org", capturedApiIntent.getStringExtra(OpenPgpApi.EXTRA_SENDER_ADDRESS)); + assertPartAnnotationHasState(message, messageCryptoCallback, CryptoError.OPENPGP_OK, decryptedPart, + decryptionResult, signatureResult, pendingIntent); + } + + private void processEncryptedMessageAndCaptureMocks(Message message, Body encryptedBody, OutputStream outputStream) + throws Exception { + messageCryptoHelper.asyncStartOrResumeProcessingMessage(message, messageCryptoCallback, null); + + ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); + ArgumentCaptor dataSourceCaptor = ArgumentCaptor.forClass(OpenPgpDataSource.class); + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(IOpenPgpSinkResultCallback.class); + verify(openPgpApi).executeApiAsync(intentCaptor.capture(), dataSourceCaptor.capture(), + any(OpenPgpDataSink.class), callbackCaptor.capture()); + + capturedApiIntent = intentCaptor.getValue(); + capturedCallback = callbackCaptor.getValue(); + + OpenPgpDataSource dataSource = dataSourceCaptor.getValue(); + dataSource.writeTo(outputStream); + verify(encryptedBody).writeTo(outputStream); + } + + private void processSignedMessageAndCaptureMocks(Message message, BodyPart signedBodyPart, + OutputStream outputStream) throws Exception { + messageCryptoHelper.asyncStartOrResumeProcessingMessage(message, messageCryptoCallback, null); + + ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); + ArgumentCaptor dataSourceCaptor = ArgumentCaptor.forClass(OpenPgpDataSource.class); + ArgumentCaptor callbackCaptor = ArgumentCaptor.forClass(IOpenPgpSinkResultCallback.class); + verify(openPgpApi).executeApiAsync(intentCaptor.capture(), dataSourceCaptor.capture(), + callbackCaptor.capture()); + + capturedApiIntent = intentCaptor.getValue(); + capturedCallback = callbackCaptor.getValue(); + + OpenPgpDataSource dataSource = dataSourceCaptor.getValue(); + dataSource.writeTo(outputStream); + verify(signedBodyPart).writeTo(outputStream); + } + + private void assertPartAnnotationHasState(Message message, MessageCryptoCallback messageCryptoCallback, + CryptoError cryptoErrorState, MimeBodyPart replacementPart, OpenPgpDecryptionResult openPgpDecryptionResult, + OpenPgpSignatureResult openPgpSignatureResult, PendingIntent openPgpPendingIntent) { + ArgumentCaptor captor = ArgumentCaptor.forClass(MessageCryptoAnnotations.class); + verify(messageCryptoCallback).onCryptoOperationsFinished(captor.capture()); + MessageCryptoAnnotations annotations = captor.getValue(); + CryptoResultAnnotation cryptoResultAnnotation = annotations.get(message); + assertEquals(cryptoErrorState, cryptoResultAnnotation.getErrorType()); + if (replacementPart != null) { + assertSame(replacementPart, cryptoResultAnnotation.getReplacementData()); + } + assertSame(openPgpDecryptionResult, cryptoResultAnnotation.getOpenPgpDecryptionResult()); + assertSame(openPgpSignatureResult, cryptoResultAnnotation.getOpenPgpSignatureResult()); + assertSame(openPgpPendingIntent, cryptoResultAnnotation.getOpenPgpPendingIntent()); + verifyNoMoreInteractions(messageCryptoCallback); + } + +} \ No newline at end of file