diff --git a/app/core/src/main/java/com/fsck/k9/message/AutocryptStatusInteractor.java b/app/core/src/main/java/com/fsck/k9/message/AutocryptStatusInteractor.java index a6e7216cb..f76e998be 100644 --- a/app/core/src/main/java/com/fsck/k9/message/AutocryptStatusInteractor.java +++ b/app/core/src/main/java/com/fsck/k9/message/AutocryptStatusInteractor.java @@ -5,6 +5,7 @@ import java.io.InputStream; import android.app.PendingIntent; import android.content.Intent; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.support.annotation.WorkerThread; @@ -51,7 +52,7 @@ public class AutocryptStatusInteractor { } } - @Nullable + @NonNull private RecipientAutocryptStatusType getRecipientAutocryptStatusFromIntent(Intent result) { boolean allKeysConfirmed = result.getBooleanExtra(OpenPgpApi.RESULT_KEYS_CONFIRMED, false); int autocryptStatus = diff --git a/app/core/src/main/java/com/fsck/k9/message/CryptoStatus.kt b/app/core/src/main/java/com/fsck/k9/message/CryptoStatus.kt index 0592ce411..189b0fe09 100644 --- a/app/core/src/main/java/com/fsck/k9/message/CryptoStatus.kt +++ b/app/core/src/main/java/com/fsck/k9/message/CryptoStatus.kt @@ -1,16 +1,16 @@ package com.fsck.k9.message interface CryptoStatus { - fun getOpenPgpKeyId(): Long? + val openPgpKeyId: Long? fun isProviderStateOk(): Boolean - fun isSenderPreferEncryptMutual(): Boolean + val isSenderPreferEncryptMutual: Boolean + val isEncryptionEnabled: Boolean + val isPgpInlineModeEnabled: Boolean + val isSignOnly: Boolean fun isSigningEnabled(): Boolean - fun isEncryptionEnabled(): Boolean - fun isPgpInlineModeEnabled(): Boolean - fun isSignOnly(): Boolean fun isUserChoice(): Boolean - fun isReplyToEncrypted(): Boolean + val isReplyToEncrypted: Boolean fun hasRecipients(): Boolean - fun isEncryptSubject(): Boolean + val isEncryptSubject: Boolean fun getRecipientAddresses(): Array } diff --git a/app/core/src/test/java/com/fsck/k9/message/PgpMessageBuilderTest.java b/app/core/src/test/java/com/fsck/k9/message/PgpMessageBuilderTest.java deleted file mode 100644 index a21f52013..000000000 --- a/app/core/src/test/java/com/fsck/k9/message/PgpMessageBuilderTest.java +++ /dev/null @@ -1,623 +0,0 @@ -package com.fsck.k9.message; - - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.List; - -import android.app.Activity; -import android.app.PendingIntent; -import android.content.Intent; -import android.os.Bundle; - -import com.fsck.k9.Account.QuoteStyle; -import com.fsck.k9.Identity; -import com.fsck.k9.K9RobolectricTest; -import com.fsck.k9.TestCoreResourceProvider; -import com.fsck.k9.autocrypt.AutocryptOpenPgpApiInteractor; -import com.fsck.k9.autocrypt.AutocryptOperations; -import com.fsck.k9.mail.Address; -import com.fsck.k9.mail.BodyPart; -import com.fsck.k9.mail.BoundaryGenerator; -import com.fsck.k9.mail.MessagingException; -import com.fsck.k9.mail.internet.BinaryTempFileBody; -import com.fsck.k9.mail.internet.MessageIdGenerator; -import com.fsck.k9.mail.internet.MimeMessage; -import com.fsck.k9.mail.internet.MimeMultipart; -import com.fsck.k9.mail.internet.MimeUtility; -import com.fsck.k9.mail.internet.TextBody; -import com.fsck.k9.message.MessageBuilder.Callback; -import com.fsck.k9.message.quote.InsertableHtmlContent; -import org.apache.commons.io.Charsets; -import org.apache.commons.io.IOUtils; -import org.apache.james.mime4j.util.MimeUtil; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; -import org.openintents.openpgp.util.OpenPgpApi; -import org.openintents.openpgp.util.OpenPgpApi.OpenPgpDataSource; - -import static com.fsck.k9.autocrypt.AutocryptOperationsHelper.assertMessageHasAutocryptHeader; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.nullable; -import static org.mockito.ArgumentMatchers.same; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -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; - - -public class PgpMessageBuilderTest extends K9RobolectricTest { - private static final long TEST_KEY_ID = 123L; - private static final String TEST_MESSAGE_TEXT = "message text with a ☭ CCCP symbol"; - private static final byte[] AUTOCRYPT_KEY_MATERIAL = { 1, 2, 3 }; - private static final String SENDER_EMAIL = "test@example.org"; - - - private CryptoStatus cryptoStatus = createCryptoStatus(); - private OpenPgpApi openPgpApi = mock(OpenPgpApi.class); - private AutocryptOpenPgpApiInteractor autocryptOpenPgpApiInteractor = mock(AutocryptOpenPgpApiInteractor.class); - private PgpMessageBuilder pgpMessageBuilder = createDefaultPgpMessageBuilder(openPgpApi, - autocryptOpenPgpApiInteractor, cryptoStatus); - - @Before - public void setUp() throws Exception { - when(autocryptOpenPgpApiInteractor.getKeyMaterialForKeyId(openPgpApi, TEST_KEY_ID, SENDER_EMAIL)) - .thenReturn(AUTOCRYPT_KEY_MATERIAL); - } - - @Test - public void build__withCryptoProviderNotOk__shouldThrow() { - configureEncryptAndSign(); - when(cryptoStatus.isProviderStateOk()).thenReturn(false); - - Callback mockCallback = mock(Callback.class); - pgpMessageBuilder.buildAsync(mockCallback); - - verify(mockCallback).onMessageBuildException(any(MessagingException.class)); - verifyNoMoreInteractions(mockCallback); - } - - @Test - public void buildCleartext__withNoSigningKey__shouldBuildTrivialMessage() { - configureCleartext(); - when(cryptoStatus.getOpenPgpKeyId()).thenReturn(null); - - Callback mockCallback = mock(Callback.class); - pgpMessageBuilder.buildAsync(mockCallback); - - ArgumentCaptor captor = ArgumentCaptor.forClass(MimeMessage.class); - verify(mockCallback).onMessageBuildSuccess(captor.capture(), eq(false)); - verifyNoMoreInteractions(mockCallback); - - MimeMessage message = captor.getValue(); - assertEquals("text/plain", message.getMimeType()); - } - - @Test - public void buildCleartext__shouldSucceed() { - configureCleartext(); - - Callback mockCallback = mock(Callback.class); - pgpMessageBuilder.buildAsync(mockCallback); - - ArgumentCaptor captor = ArgumentCaptor.forClass(MimeMessage.class); - verify(mockCallback).onMessageBuildSuccess(captor.capture(), eq(false)); - verifyNoMoreInteractions(mockCallback); - - MimeMessage message = captor.getValue(); - assertMessageHasAutocryptHeader(message, SENDER_EMAIL, false, AUTOCRYPT_KEY_MATERIAL); - } - - @Test - public void buildSign__withNoDetachedSignatureInResult__shouldThrow() { - configureSignOnly(); - - Intent returnIntent = new Intent(); - returnIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); - when(openPgpApi.executeApi(any(Intent.class), any(OpenPgpDataSource.class), nullable(OutputStream.class))) - .thenReturn(returnIntent); - - Callback mockCallback = mock(Callback.class); - pgpMessageBuilder.buildAsync(mockCallback); - - verify(mockCallback).onMessageBuildException(any(MessagingException.class)); - verifyNoMoreInteractions(mockCallback); - } - - @Test - public void buildSign__withDetachedSignatureInResult__shouldSucceed() { - configureSignOnly(); - - ArgumentCaptor capturedApiIntent = ArgumentCaptor.forClass(Intent.class); - - Intent returnIntent = new Intent(); - returnIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); - returnIntent.putExtra(OpenPgpApi.RESULT_DETACHED_SIGNATURE, new byte[] { 1, 2, 3 }); - when(openPgpApi.executeApi(capturedApiIntent.capture(), any(OpenPgpDataSource.class), - nullable(OutputStream.class))).thenReturn(returnIntent); - - Callback mockCallback = mock(Callback.class); - pgpMessageBuilder.buildAsync(mockCallback); - - Intent expectedIntent = new Intent(OpenPgpApi.ACTION_DETACHED_SIGN); - expectedIntent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, TEST_KEY_ID); - expectedIntent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); - assertIntentEqualsActionAndExtras(expectedIntent, capturedApiIntent.getValue()); - - ArgumentCaptor captor = ArgumentCaptor.forClass(MimeMessage.class); - verify(mockCallback).onMessageBuildSuccess(captor.capture(), eq(false)); - verifyNoMoreInteractions(mockCallback); - - MimeMessage message = captor.getValue(); - Assert.assertEquals("message must be multipart/signed", "multipart/signed", message.getMimeType()); - - MimeMultipart multipart = (MimeMultipart) message.getBody(); - Assert.assertEquals("multipart/signed must consist of two parts", 2, multipart.getCount()); - - BodyPart contentBodyPart = multipart.getBodyPart(0); - Assert.assertEquals("first part must have content type text/plain", - "text/plain", MimeUtility.getHeaderParameter(contentBodyPart.getContentType(), null)); - assertTrue("signed message body must be TextBody", contentBodyPart.getBody() instanceof TextBody); - Assert.assertEquals(MimeUtil.ENC_QUOTED_PRINTABLE, ((TextBody) contentBodyPart.getBody()).getEncoding()); - assertContentOfBodyPartEquals("content must match the message text", contentBodyPart, TEST_MESSAGE_TEXT); - - BodyPart signatureBodyPart = multipart.getBodyPart(1); - String contentType = signatureBodyPart.getContentType(); - Assert.assertEquals("second part must be pgp signature", "application/pgp-signature", - MimeUtility.getHeaderParameter(contentType, null)); - Assert.assertEquals("second part must be called signature.asc", "signature.asc", - MimeUtility.getHeaderParameter(contentType, "name")); - assertContentOfBodyPartEquals("content must match the supplied detached signature", - signatureBodyPart, new byte[] { 1, 2, 3 }); - - assertMessageHasAutocryptHeader(message, SENDER_EMAIL, false, AUTOCRYPT_KEY_MATERIAL); - } - - @Test - public void buildSign__withUserInteractionResult__shouldReturnUserInteraction() { - configureSignOnly(); - - Intent returnIntent = mock(Intent.class); - when(returnIntent.getIntExtra(eq(OpenPgpApi.RESULT_CODE), anyInt())) - .thenReturn(OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED); - final PendingIntent mockPendingIntent = mock(PendingIntent.class); - when(returnIntent.getParcelableExtra(eq(OpenPgpApi.RESULT_INTENT))) - .thenReturn(mockPendingIntent); - - when(openPgpApi.executeApi(any(Intent.class), any(OpenPgpDataSource.class), nullable(OutputStream.class))) - .thenReturn(returnIntent); - - Callback mockCallback = mock(Callback.class); - pgpMessageBuilder.buildAsync(mockCallback); - - ArgumentCaptor captor = ArgumentCaptor.forClass(PendingIntent.class); - verify(mockCallback).onMessageBuildReturnPendingIntent(captor.capture(), anyInt()); - verifyNoMoreInteractions(mockCallback); - - PendingIntent pendingIntent = captor.getValue(); - Assert.assertSame(pendingIntent, mockPendingIntent); - } - - @Test - public void buildSign__withReturnAfterUserInteraction__shouldSucceed() { - configureSignOnly(); - - int returnedRequestCode; - { - Intent returnIntent = spy(new Intent()); - returnIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED); - - PendingIntent mockPendingIntent = mock(PendingIntent.class); - when(returnIntent.getParcelableExtra(eq(OpenPgpApi.RESULT_INTENT))) - .thenReturn(mockPendingIntent); - - when(openPgpApi.executeApi(any(Intent.class), any(OpenPgpDataSource.class), nullable(OutputStream.class))) - .thenReturn(returnIntent); - - Callback mockCallback = mock(Callback.class); - pgpMessageBuilder.buildAsync(mockCallback); - - verify(returnIntent).getIntExtra(eq(OpenPgpApi.RESULT_CODE), anyInt()); - ArgumentCaptor piCaptor = ArgumentCaptor.forClass(PendingIntent.class); - ArgumentCaptor rcCaptor = ArgumentCaptor.forClass(Integer.class); - verify(mockCallback).onMessageBuildReturnPendingIntent(piCaptor.capture(), rcCaptor.capture()); - verifyNoMoreInteractions(mockCallback); - - returnedRequestCode = rcCaptor.getValue(); - Assert.assertSame(mockPendingIntent, piCaptor.getValue()); - } - - { - Intent returnIntent = spy(new Intent()); - returnIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); - - Intent mockReturnIntent = mock(Intent.class); - when(openPgpApi.executeApi(same(mockReturnIntent), any(OpenPgpDataSource.class), - nullable(OutputStream.class))).thenReturn(returnIntent); - - Callback mockCallback = mock(Callback.class); - pgpMessageBuilder.onActivityResult(returnedRequestCode, Activity.RESULT_OK, mockReturnIntent, mockCallback); - verify(openPgpApi).executeApi(same(mockReturnIntent), any(OpenPgpDataSource.class), - nullable(OutputStream.class)); - verify(returnIntent).getIntExtra(eq(OpenPgpApi.RESULT_CODE), anyInt()); - } - } - - @Test - public void buildEncrypt__withoutRecipients__shouldThrow() { - configureEncryptAndSign(); - - Intent returnIntent = spy(new Intent()); - returnIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); - when(openPgpApi.executeApi(any(Intent.class), any(OpenPgpDataSource.class), nullable(OutputStream.class))) - .thenReturn(returnIntent); - - Callback mockCallback = mock(Callback.class); - pgpMessageBuilder.buildAsync(mockCallback); - - verify(mockCallback).onMessageBuildException(any(MessagingException.class)); - verifyNoMoreInteractions(mockCallback); - } - - @Test - public void buildEncrypt__checkGossip() { - configureEncryptAndSign(); - configureRecipients("alice@example.org", "bob@example.org"); - - Intent returnIntent = new Intent(); - returnIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); - when(openPgpApi.executeApi(any(Intent.class), any(OpenPgpDataSource.class), nullable(OutputStream.class))) - .thenReturn(returnIntent); - pgpMessageBuilder.buildAsync(mock(Callback.class)); - - verify(autocryptOpenPgpApiInteractor).getKeyMaterialForUserId(same(openPgpApi), eq("alice@example.org")); - verify(autocryptOpenPgpApiInteractor).getKeyMaterialForUserId(same(openPgpApi), eq("bob@example.org")); - } - - @Test - public void buildEncrypt__checkGossip__filterBcc() { - configureEncryptAndSign(); - configureRecipients("alice@example.org", "bob@example.org", "carol@example.org"); - pgpMessageBuilder.setBcc(Collections.singletonList(new Address("carol@example.org"))); - - Intent returnIntent = new Intent(); - returnIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); - when(openPgpApi.executeApi(any(Intent.class), any(OpenPgpDataSource.class), nullable(OutputStream.class))) - .thenReturn(returnIntent); - pgpMessageBuilder.buildAsync(mock(Callback.class)); - - verify(autocryptOpenPgpApiInteractor).getKeyMaterialForUserId(same(openPgpApi), eq("alice@example.org")); - verify(autocryptOpenPgpApiInteractor).getKeyMaterialForUserId(same(openPgpApi), eq("bob@example.org")); - } - - @Test - public void buildEncrypt__checkGossip__filterBccSingleRecipient() { - configureEncryptAndSign(); - configureRecipients("alice@example.org", "carol@example.org"); - pgpMessageBuilder.setBcc(Collections.singletonList(new Address("carol@example.org"))); - - Intent returnIntent = new Intent(); - returnIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); - when(openPgpApi.executeApi(any(Intent.class), any(OpenPgpDataSource.class), nullable(OutputStream.class))) - .thenReturn(returnIntent); - pgpMessageBuilder.buildAsync(mock(Callback.class)); - - verify(autocryptOpenPgpApiInteractor).getKeyMaterialForKeyId(any(OpenPgpApi.class), any(Long.class), any(String.class)); - verifyNoMoreInteractions(autocryptOpenPgpApiInteractor); - } - - @Test - public void buildEncrypt__shouldSucceed() { - configureEncryptAndSign(); - configureRecipients("test@example.org"); - - ArgumentCaptor capturedApiIntent = ArgumentCaptor.forClass(Intent.class); - - Intent returnIntent = new Intent(); - returnIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); - - when(openPgpApi.executeApi(capturedApiIntent.capture(), any(OpenPgpDataSource.class), - nullable(OutputStream.class))).thenReturn(returnIntent); - - Callback mockCallback = mock(Callback.class); - pgpMessageBuilder.buildAsync(mockCallback); - - Intent expectedApiIntent = new Intent(OpenPgpApi.ACTION_SIGN_AND_ENCRYPT); - expectedApiIntent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, TEST_KEY_ID); - expectedApiIntent.putExtra(OpenPgpApi.EXTRA_KEY_IDS, new long[] { TEST_KEY_ID }); - expectedApiIntent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); - expectedApiIntent.putExtra(OpenPgpApi.EXTRA_USER_IDS, cryptoStatus.getRecipientAddresses()); - assertIntentEqualsActionAndExtras(expectedApiIntent, capturedApiIntent.getValue()); - - ArgumentCaptor captor = ArgumentCaptor.forClass(MimeMessage.class); - verify(mockCallback).onMessageBuildSuccess(captor.capture(), eq(false)); - verifyNoMoreInteractions(mockCallback); - - MimeMessage message = captor.getValue(); - - Assert.assertEquals("message must be multipart/encrypted", "multipart/encrypted", message.getMimeType()); - - MimeMultipart multipart = (MimeMultipart) message.getBody(); - Assert.assertEquals("multipart/encrypted must consist of two parts", 2, multipart.getCount()); - - BodyPart dummyBodyPart = multipart.getBodyPart(0); - Assert.assertEquals("first part must be pgp encrypted dummy part", - "application/pgp-encrypted", dummyBodyPart.getContentType()); - assertContentOfBodyPartEquals("content must match the supplied detached signature", - dummyBodyPart, "Version: 1"); - - BodyPart encryptedBodyPart = multipart.getBodyPart(1); - Assert.assertEquals("second part must be octet-stream of encrypted data", - "application/octet-stream; name=\"encrypted.asc\"", encryptedBodyPart.getContentType()); - assertTrue("message body must be BinaryTempFileBody", - encryptedBodyPart.getBody() instanceof BinaryTempFileBody); - Assert.assertEquals(MimeUtil.ENC_7BIT, ((BinaryTempFileBody) encryptedBodyPart.getBody()).getEncoding()); - - assertMessageHasAutocryptHeader(message, SENDER_EMAIL, false, AUTOCRYPT_KEY_MATERIAL); - } - - @Test - public void buildEncrypt__withInlineEnabled__shouldSucceed() { - configureEncryptAndSign(); - configureRecipients("test@example.org"); - configurePgpInline(); - - ArgumentCaptor capturedApiIntent = ArgumentCaptor.forClass(Intent.class); - - Intent returnIntent = new Intent(); - returnIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); - - when(openPgpApi.executeApi(capturedApiIntent.capture(), any(OpenPgpDataSource.class), - nullable(OutputStream.class))).thenReturn(returnIntent); - - Callback mockCallback = mock(Callback.class); - pgpMessageBuilder.buildAsync(mockCallback); - - Intent expectedApiIntent = new Intent(OpenPgpApi.ACTION_SIGN_AND_ENCRYPT); - expectedApiIntent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, TEST_KEY_ID); - expectedApiIntent.putExtra(OpenPgpApi.EXTRA_KEY_IDS, new long[] { TEST_KEY_ID }); - expectedApiIntent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); - expectedApiIntent.putExtra(OpenPgpApi.EXTRA_USER_IDS, cryptoStatus.getRecipientAddresses()); - assertIntentEqualsActionAndExtras(expectedApiIntent, capturedApiIntent.getValue()); - - ArgumentCaptor captor = ArgumentCaptor.forClass(MimeMessage.class); - verify(mockCallback).onMessageBuildSuccess(captor.capture(), eq(false)); - verifyNoMoreInteractions(mockCallback); - - MimeMessage message = captor.getValue(); - Assert.assertEquals("text/plain", message.getMimeType()); - assertTrue("message body must be BinaryTempFileBody", message.getBody() instanceof BinaryTempFileBody); - Assert.assertEquals(MimeUtil.ENC_7BIT, ((BinaryTempFileBody) message.getBody()).getEncoding()); - - assertMessageHasAutocryptHeader(message, SENDER_EMAIL, false, AUTOCRYPT_KEY_MATERIAL); - } - - @Test - public void buildSign__withInlineEnabled__shouldSucceed() { - configureSignOnly(); - configureRecipients("test@example.org"); - configurePgpInline(); - - ArgumentCaptor capturedApiIntent = ArgumentCaptor.forClass(Intent.class); - - Intent returnIntent = new Intent(); - returnIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); - - when(openPgpApi.executeApi(capturedApiIntent.capture(), any(OpenPgpDataSource.class), - nullable(OutputStream.class))).thenReturn(returnIntent); - - Callback mockCallback = mock(Callback.class); - pgpMessageBuilder.buildAsync(mockCallback); - - Intent expectedApiIntent = new Intent(OpenPgpApi.ACTION_SIGN); - expectedApiIntent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, TEST_KEY_ID); - expectedApiIntent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); - assertIntentEqualsActionAndExtras(expectedApiIntent, capturedApiIntent.getValue()); - - ArgumentCaptor captor = ArgumentCaptor.forClass(MimeMessage.class); - verify(mockCallback).onMessageBuildSuccess(captor.capture(), eq(false)); - verifyNoMoreInteractions(mockCallback); - - MimeMessage message = captor.getValue(); - Assert.assertEquals("message must be text/plain", "text/plain", message.getMimeType()); - - assertMessageHasAutocryptHeader(message, SENDER_EMAIL, false, AUTOCRYPT_KEY_MATERIAL); - } - - @Test - public void buildSignWithAttach__withInlineEnabled__shouldThrow() { - configureSignOnly(); - configurePgpInline(); - pgpMessageBuilder.setAttachments(createAttachmentList()); - - Callback mockCallback = mock(Callback.class); - pgpMessageBuilder.buildAsync(mockCallback); - - verify(mockCallback).onMessageBuildException(any(MessagingException.class)); - verifyNoMoreInteractions(mockCallback); - verifyNoMoreInteractions(openPgpApi); - } - - @Test - public void buildEncryptWithAttach__withInlineEnabled__shouldThrow() { - configureEncryptAndSign(); - configurePgpInline(); - pgpMessageBuilder.setAttachments(createAttachmentList()); - - Callback mockCallback = mock(Callback.class); - pgpMessageBuilder.buildAsync(mockCallback); - - verify(mockCallback).onMessageBuildException(any(MessagingException.class)); - verifyNoMoreInteractions(mockCallback); - verifyNoMoreInteractions(openPgpApi); - } - - @Test - public void buildSign__withNoDetachedSignatureExtra__shouldFail() { - configureSignOnly(); - - Intent returnIntentSigned = new Intent(); - returnIntentSigned.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS); - // no OpenPgpApi.EXTRA_DETACHED_SIGNATURE! - - - when(openPgpApi.executeApi(any(Intent.class), any(OpenPgpDataSource.class), nullable(OutputStream.class))) - .thenReturn(returnIntentSigned); - Callback mockCallback = mock(Callback.class); - pgpMessageBuilder.buildAsync(mockCallback); - - - verify(mockCallback).onMessageBuildException(any(MessagingException.class)); - verifyNoMoreInteractions(mockCallback); - } - - private CryptoStatus createCryptoStatus() { - CryptoStatus cryptoStatus = mock(CryptoStatus.class); - when(cryptoStatus.isPgpInlineModeEnabled()).thenReturn(false); - when(cryptoStatus.isSenderPreferEncryptMutual()).thenReturn(false); - when(cryptoStatus.isEncryptSubject()).thenReturn(true); - when(cryptoStatus.getOpenPgpKeyId()).thenReturn(TEST_KEY_ID); - when(cryptoStatus.getRecipientAddresses()).thenReturn(new String[0]); - when(cryptoStatus.hasRecipients()).thenReturn(false); - when(cryptoStatus.isProviderStateOk()).thenReturn(true); - return cryptoStatus; - } - - private void configureEncryptAndSign() { - when(cryptoStatus.isEncryptionEnabled()).thenReturn(true); - when(cryptoStatus.isSigningEnabled()).thenReturn(true); - } - - private void configureSignOnly() { - when(cryptoStatus.isEncryptionEnabled()).thenReturn(false); - when(cryptoStatus.isSigningEnabled()).thenReturn(true); - } - - private void configureCleartext() { - when(cryptoStatus.isEncryptionEnabled()).thenReturn(false); - when(cryptoStatus.isSigningEnabled()).thenReturn(false); - } - - private void configurePgpInline() { - when(cryptoStatus.isPgpInlineModeEnabled()).thenReturn(true); - } - - private void configureRecipients(String... recipients) { - when(cryptoStatus.hasRecipients()).thenReturn(true); - when(cryptoStatus.getRecipientAddresses()).thenReturn(recipients); - } - - private static PgpMessageBuilder createDefaultPgpMessageBuilder(OpenPgpApi openPgpApi, - AutocryptOpenPgpApiInteractor autocryptOpenPgpApiInteractor, CryptoStatus cryptoStatus) { - PgpMessageBuilder builder = new PgpMessageBuilder( - MessageIdGenerator.getInstance(), BoundaryGenerator.getInstance(), - AutocryptOperations.getInstance(), autocryptOpenPgpApiInteractor, new TestCoreResourceProvider()); - builder.setOpenPgpApi(openPgpApi); - builder.setCryptoStatus(cryptoStatus); - - Identity identity = new Identity(); - identity.setName("tester"); - identity.setEmail(SENDER_EMAIL); - identity.setDescription("test identity"); - identity.setSignatureUse(false); - - builder.setSubject("subject") - .setSentDate(new Date()) - .setHideTimeZone(false) - .setTo(new ArrayList
()) - .setCc(new ArrayList
()) - .setBcc(new ArrayList
()) - .setInReplyTo("inreplyto") - .setReferences("references") - .setRequestReadReceipt(false) - .setIdentity(identity) - .setMessageFormat(SimpleMessageFormat.TEXT) - .setText(TEST_MESSAGE_TEXT) - .setAttachments(new ArrayList()) - .setSignature("signature") - .setQuoteStyle(QuoteStyle.PREFIX) - .setQuotedTextMode(QuotedTextMode.NONE) - .setQuotedText("quoted text") - .setQuotedHtmlContent(new InsertableHtmlContent()) - .setReplyAfterQuote(false) - .setSignatureBeforeQuotedText(false) - .setIdentityChanged(false) - .setSignatureChanged(false) - .setCursorPosition(0) - .setMessageReference(null) - .setDraft(false); - - return builder; - } - - private static List createAttachmentList() { - Attachment attachment = mock(Attachment.class); - when(attachment.getState()).thenReturn(Attachment.LoadingState.URI_ONLY); - return Collections.singletonList(attachment); - } - - private static void assertContentOfBodyPartEquals(String reason, BodyPart signatureBodyPart, byte[] expected) { - try { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - signatureBodyPart.getBody().writeTo(bos); - Assert.assertArrayEquals(reason, expected, bos.toByteArray()); - } catch (IOException | MessagingException e) { - Assert.fail(); - } - } - - private static void assertContentOfBodyPartEquals(String reason, BodyPart signatureBodyPart, String expected) { - try { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - InputStream inputStream = MimeUtility.decodeBody(signatureBodyPart.getBody()); - IOUtils.copy(inputStream, bos); - Assert.assertEquals(reason, expected, new String(bos.toByteArray(), Charsets.UTF_8)); - } catch (IOException | MessagingException e) { - Assert.fail(); - } - } - - private static void assertIntentEqualsActionAndExtras(Intent expected, Intent actual) { - Assert.assertEquals(expected.getAction(), actual.getAction()); - - Bundle expectedExtras = expected.getExtras(); - Bundle intentExtras = actual.getExtras(); - - if (expectedExtras.size() != intentExtras.size()) { - Assert.assertEquals(expectedExtras.size(), intentExtras.size()); - } - - for (String key : expectedExtras.keySet()) { - Object intentExtra = intentExtras.get(key); - Object expectedExtra = expectedExtras.get(key); - if (intentExtra == null) { - if (expectedExtra == null) { - continue; - } - Assert.fail("found null for an expected non-null extra: " + key); - } - if (intentExtra instanceof long[]) { - if (!Arrays.equals((long[]) intentExtra, (long[]) expectedExtra)) { - Assert.assertArrayEquals("error in " + key, (long[]) expectedExtra, (long[]) intentExtra); - } - } else { - if (!intentExtra.equals(expectedExtra)) { - Assert.assertEquals("error in " + key, expectedExtra, intentExtra); - } - } - } - } -} diff --git a/app/core/src/test/java/com/fsck/k9/message/PgpMessageBuilderTest.kt b/app/core/src/test/java/com/fsck/k9/message/PgpMessageBuilderTest.kt new file mode 100644 index 000000000..07ba6e2c4 --- /dev/null +++ b/app/core/src/test/java/com/fsck/k9/message/PgpMessageBuilderTest.kt @@ -0,0 +1,672 @@ +package com.fsck.k9.message + + +import android.app.Activity +import android.app.PendingIntent +import android.content.Intent +import android.os.Parcelable +import com.fsck.k9.Account.QuoteStyle +import com.fsck.k9.Identity +import com.fsck.k9.RobolectricTest +import com.fsck.k9.activity.compose.ComposeCryptoStatus +import com.fsck.k9.activity.compose.RecipientPresenter.CryptoMode +import com.fsck.k9.activity.misc.Attachment +import com.fsck.k9.autocrypt.AutocryptOpenPgpApiInteractor +import com.fsck.k9.autocrypt.AutocryptOperations +import com.fsck.k9.autocrypt.AutocryptOperationsHelper.assertMessageHasAutocryptHeader +import com.fsck.k9.mail.Address +import com.fsck.k9.mail.BodyPart +import com.fsck.k9.mail.BoundaryGenerator +import com.fsck.k9.mail.MessagingException +import com.fsck.k9.mail.internet.* +import com.fsck.k9.message.MessageBuilder.Callback +import com.fsck.k9.message.quote.InsertableHtmlContent +import com.fsck.k9.view.RecipientSelectView +import com.nhaarman.mockito_kotlin.anyOrNull +import org.apache.commons.io.Charsets +import org.apache.commons.io.IOUtils +import org.apache.james.mime4j.util.MimeUtil +import org.junit.Assert +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentCaptor +import org.mockito.Mockito.* +import org.openintents.openpgp.OpenPgpApiManager.OpenPgpProviderState +import org.openintents.openpgp.OpenPgpError +import org.openintents.openpgp.util.OpenPgpApi +import org.openintents.openpgp.util.OpenPgpApi.OpenPgpDataSource +import org.robolectric.RuntimeEnvironment +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.OutputStream +import java.util.* + + +class PgpMessageBuilderTest : RobolectricTest() { + + + private val defaultCryptoStatus = ComposeCryptoStatus( + OpenPgpProviderState.OK, + TEST_KEY_ID, + emptyList(), + false, + false, + false, + true, + CryptoMode.NO_CHOICE + ) + private val openPgpApi = mock(OpenPgpApi::class.java) + private val autocryptOpenPgpApiInteractor = mock(AutocryptOpenPgpApiInteractor::class.java) + private val pgpMessageBuilder = createDefaultPgpMessageBuilder(openPgpApi, autocryptOpenPgpApiInteractor) + + @Before + @Throws(Exception::class) + fun setUp() { + BinaryTempFileBody.setTempDirectory(RuntimeEnvironment.application.cacheDir) + `when`(autocryptOpenPgpApiInteractor.getKeyMaterialForKeyId(openPgpApi, TEST_KEY_ID, SENDER_EMAIL)) + .thenReturn(AUTOCRYPT_KEY_MATERIAL) + } + + @Test + @Throws(MessagingException::class) + fun build__withCryptoProviderUnconfigured__shouldThrow() { + val cryptoStatus = defaultCryptoStatus.copy(openPgpProviderState = OpenPgpProviderState.UNCONFIGURED) + + pgpMessageBuilder.setCryptoStatus(cryptoStatus) + + val mockCallback = mock(Callback::class.java) + pgpMessageBuilder.buildAsync(mockCallback) + + verify(mockCallback).onMessageBuildException(any()) + verifyNoMoreInteractions(mockCallback) + } + + @Test + @Throws(MessagingException::class) + fun build__withCryptoProviderUninitialized__shouldThrow() { + val cryptoStatus = defaultCryptoStatus.copy(openPgpProviderState = OpenPgpProviderState.UNINITIALIZED) + + pgpMessageBuilder.setCryptoStatus(cryptoStatus) + + val mockCallback = mock(Callback::class.java) + pgpMessageBuilder.buildAsync(mockCallback) + + verify(mockCallback).onMessageBuildException(any()) + verifyNoMoreInteractions(mockCallback) + } + + @Test + @Throws(MessagingException::class) + fun build__withCryptoProviderError__shouldThrow() { + val cryptoStatus = defaultCryptoStatus.copy(openPgpProviderState = OpenPgpProviderState.ERROR) + + pgpMessageBuilder.setCryptoStatus(cryptoStatus) + + val mockCallback = mock(Callback::class.java) + pgpMessageBuilder.buildAsync(mockCallback) + + verify(mockCallback).onMessageBuildException(any()) + verifyNoMoreInteractions(mockCallback) + } + + @Test + fun buildCleartext__withNoSigningKey__shouldBuildTrivialMessage() { + val cryptoStatus = defaultCryptoStatus.copy(openPgpKeyId = null) + + pgpMessageBuilder.setCryptoStatus(cryptoStatus) + + val mockCallback = mock(Callback::class.java) + pgpMessageBuilder.buildAsync(mockCallback) + + val captor = ArgumentCaptor.forClass(MimeMessage::class.java) + verify(mockCallback).onMessageBuildSuccess(captor.capture(), eq(false)) + verifyNoMoreInteractions(mockCallback) + + val message = captor.value + assertEquals("text/plain", message.mimeType) + } + + @Test + fun buildCleartext__shouldSucceed() { + pgpMessageBuilder.setCryptoStatus(defaultCryptoStatus) + + val mockCallback = mock(Callback::class.java) + pgpMessageBuilder.buildAsync(mockCallback) + + val captor = ArgumentCaptor.forClass(MimeMessage::class.java) + verify(mockCallback).onMessageBuildSuccess(captor.capture(), eq(false)) + verifyNoMoreInteractions(mockCallback) + + val message = captor.value + assertMessageHasAutocryptHeader(message, SENDER_EMAIL, false, AUTOCRYPT_KEY_MATERIAL) + } + + @Test + @Throws(MessagingException::class) + fun buildSign__withNoDetachedSignatureInResult__shouldThrow() { + val cryptoStatus = defaultCryptoStatus.copy(cryptoMode = CryptoMode.SIGN_ONLY) + pgpMessageBuilder.setCryptoStatus(cryptoStatus) + + val returnIntent = Intent() + returnIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS) + `when`(openPgpApi.executeApi(any(), any(), any())).thenReturn(returnIntent) + + val mockCallback = mock(Callback::class.java) + pgpMessageBuilder.buildAsync(mockCallback) + + verify(mockCallback).onMessageBuildException(any()) + verifyNoMoreInteractions(mockCallback) + } + + @Test + @Throws(MessagingException::class) + fun buildSign__withDetachedSignatureInResult__shouldSucceed() { + val cryptoStatus = defaultCryptoStatus.copy(cryptoMode = CryptoMode.SIGN_ONLY) + pgpMessageBuilder.setCryptoStatus(cryptoStatus) + + val capturedApiIntent = ArgumentCaptor.forClass(Intent::class.java) + + val returnIntent = Intent() + returnIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS) + returnIntent.putExtra(OpenPgpApi.RESULT_DETACHED_SIGNATURE, byteArrayOf(1, 2, 3)) + `when`(openPgpApi.executeApi(capturedApiIntent.capture(), + any(), anyOrNull())).thenReturn(returnIntent) + + val mockCallback = mock(Callback::class.java) + pgpMessageBuilder.buildAsync(mockCallback) + + val expectedIntent = Intent(OpenPgpApi.ACTION_DETACHED_SIGN) + expectedIntent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, TEST_KEY_ID) + expectedIntent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true) + assertIntentEqualsActionAndExtras(expectedIntent, capturedApiIntent.value) + + val captor = ArgumentCaptor.forClass(MimeMessage::class.java) + verify(mockCallback).onMessageBuildSuccess(captor.capture(), eq(false)) + verifyNoMoreInteractions(mockCallback) + + val message = captor.value + Assert.assertEquals("message must be multipart/signed", "multipart/signed", message.mimeType) + + val multipart = message.body as MimeMultipart + Assert.assertEquals("multipart/signed must consist of two parts", 2, multipart.count.toLong()) + + val contentBodyPart = multipart.getBodyPart(0) + Assert.assertEquals("first part must have content type text/plain", + "text/plain", MimeUtility.getHeaderParameter(contentBodyPart.contentType, null)) + assertTrue("signed message body must be TextBody", contentBodyPart.body is TextBody) + Assert.assertEquals(MimeUtil.ENC_QUOTED_PRINTABLE, (contentBodyPart.body as TextBody).encoding) + assertContentOfBodyPartEquals("content must match the message text", contentBodyPart, TEST_MESSAGE_TEXT) + + val signatureBodyPart = multipart.getBodyPart(1) + val contentType = signatureBodyPart.contentType + Assert.assertEquals("second part must be pgp signature", "application/pgp-signature", + MimeUtility.getHeaderParameter(contentType, null)) + Assert.assertEquals("second part must be called signature.asc", "signature.asc", + MimeUtility.getHeaderParameter(contentType, "name")) + assertContentOfBodyPartEquals("content must match the supplied detached signature", + signatureBodyPart, byteArrayOf(1, 2, 3)) + + assertMessageHasAutocryptHeader(message, SENDER_EMAIL, false, AUTOCRYPT_KEY_MATERIAL) + } + + @Test + @Throws(MessagingException::class) + fun buildSign__withUserInteractionResult__shouldReturnUserInteraction() { + val cryptoStatus = defaultCryptoStatus.copy(cryptoMode = CryptoMode.SIGN_ONLY) + pgpMessageBuilder.setCryptoStatus(cryptoStatus) + + val returnIntent = mock(Intent::class.java) + `when`(returnIntent.getIntExtra(eq(OpenPgpApi.RESULT_CODE), anyInt())) + .thenReturn(OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED) + val mockPendingIntent = mock(PendingIntent::class.java) + `when`(returnIntent.getParcelableExtra(eq(OpenPgpApi.RESULT_INTENT))) + .thenReturn(mockPendingIntent) + + `when`(openPgpApi.executeApi(any(), any(), any())).thenReturn(returnIntent) + + val mockCallback = mock(Callback::class.java) + pgpMessageBuilder.buildAsync(mockCallback) + + val captor = ArgumentCaptor.forClass(PendingIntent::class.java) + verify(mockCallback).onMessageBuildReturnPendingIntent(captor.capture(), anyInt()) + verifyNoMoreInteractions(mockCallback) + + val pendingIntent = captor.value + Assert.assertSame(pendingIntent, mockPendingIntent) + } + + @Test + @Throws(MessagingException::class) + fun buildSign__withReturnAfterUserInteraction__shouldSucceed() { + val cryptoStatus = defaultCryptoStatus.copy(cryptoMode = CryptoMode.SIGN_ONLY) + pgpMessageBuilder.setCryptoStatus(cryptoStatus) + + var returnedRequestCode = 0 + run { + val returnIntent = spy(Intent()) + returnIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED) + + val mockPendingIntent = mock(PendingIntent::class.java) + `when`(returnIntent.getParcelableExtra(eq(OpenPgpApi.RESULT_INTENT))) + .thenReturn(mockPendingIntent) + + `when`(openPgpApi.executeApi(any(), any(), any())).thenReturn(returnIntent) + + val mockCallback = mock(Callback::class.java) + pgpMessageBuilder.buildAsync(mockCallback) + + verify(returnIntent).getIntExtra(eq(OpenPgpApi.RESULT_CODE), anyInt()) + val piCaptor = ArgumentCaptor.forClass(PendingIntent::class.java) + val rcCaptor = ArgumentCaptor.forClass(Int::class.java) + verify(mockCallback).onMessageBuildReturnPendingIntent(piCaptor.capture(), rcCaptor.capture()) + verifyNoMoreInteractions(mockCallback) + + returnedRequestCode = rcCaptor.value + Assert.assertSame(mockPendingIntent, piCaptor.value) + } + + run { + val returnIntent = spy(Intent()) + returnIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS) + + val mockReturnIntent = mock(Intent::class.java) + `when`(openPgpApi.executeApi(any(), any(), any())).thenReturn(returnIntent) + + val mockCallback = mock(Callback::class.java) + pgpMessageBuilder.onActivityResult(returnedRequestCode, Activity.RESULT_OK, mockReturnIntent, mockCallback) + verify(openPgpApi).executeApi(same(mockReturnIntent), any(), any()) + verify(returnIntent).getIntExtra(eq(OpenPgpApi.RESULT_CODE), anyInt()) + } + } + + @Test + @Throws(MessagingException::class) + fun buildEncrypt__withoutRecipients__shouldThrow() { + val cryptoStatus = defaultCryptoStatus.copy(cryptoMode = CryptoMode.CHOICE_ENABLED) + pgpMessageBuilder.setCryptoStatus(cryptoStatus) + + val returnIntent = spy(Intent()) + returnIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS) + `when`(openPgpApi.executeApi(any(Intent::class.java), any(OpenPgpDataSource::class.java), any(OutputStream::class.java))) + .thenReturn(returnIntent) + + val mockCallback = mock(Callback::class.java) + pgpMessageBuilder.buildAsync(mockCallback) + + verify(mockCallback).onMessageBuildException(any()) + verifyNoMoreInteractions(mockCallback) + } + + @Test + @Throws(MessagingException::class) + fun buildEncrypt__checkGossip() { + val cryptoStatus = defaultCryptoStatus.copy(cryptoMode = CryptoMode.CHOICE_ENABLED, + recipientAddresses = listOf("alice@example.org", "bob@example.org")) + pgpMessageBuilder.setCryptoStatus(cryptoStatus) + + val returnIntent = Intent() + returnIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS) + `when`(openPgpApi.executeApi(any(Intent::class.java), any(OpenPgpDataSource::class.java), any(OutputStream::class.java))) + .thenReturn(returnIntent) + pgpMessageBuilder.buildAsync(mock(Callback::class.java)) + + verify(autocryptOpenPgpApiInteractor).getKeyMaterialForUserId(same(openPgpApi), eq("alice@example.org")) + verify(autocryptOpenPgpApiInteractor).getKeyMaterialForUserId(same(openPgpApi), eq("bob@example.org")) + } + + @Test + @Throws(MessagingException::class) + fun buildEncrypt__checkGossip__filterBcc() { + val cryptoStatus = defaultCryptoStatus.copy( + cryptoMode = CryptoMode.CHOICE_ENABLED, + recipientAddresses = listOf("alice@example.org", "bob@example.org", "carol@example.org")) + pgpMessageBuilder.setCryptoStatus(cryptoStatus) + pgpMessageBuilder.setBcc(listOf(Address("carol@example.org"))) + + val returnIntent = Intent() + returnIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS) + `when`(openPgpApi.executeApi(any(Intent::class.java), any(OpenPgpDataSource::class.java), any(OutputStream::class.java))) + .thenReturn(returnIntent) + pgpMessageBuilder.buildAsync(mock(Callback::class.java)) + + verify(autocryptOpenPgpApiInteractor).getKeyMaterialForKeyId(same(openPgpApi), eq(TEST_KEY_ID), eq(SENDER_EMAIL)) + verify(autocryptOpenPgpApiInteractor).getKeyMaterialForUserId(same(openPgpApi), eq("alice@example.org")) + verify(autocryptOpenPgpApiInteractor).getKeyMaterialForUserId(same(openPgpApi), eq("bob@example.org")) + } + + @Test + @Throws(MessagingException::class) + fun buildEncrypt__checkGossip__filterBccSingleRecipient() { + val cryptoStatus = defaultCryptoStatus.copy( + cryptoMode = CryptoMode.CHOICE_ENABLED, + isPgpInlineModeEnabled = true, + recipientAddresses = listOf("alice@example.org", "carol@example.org")) + pgpMessageBuilder.setCryptoStatus(cryptoStatus) + pgpMessageBuilder.setBcc(listOf(Address("carol@example.org"))) + + val returnIntent = Intent() + returnIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS) + `when`(openPgpApi.executeApi(any(Intent::class.java), any(OpenPgpDataSource::class.java), any(OutputStream::class.java))) + .thenReturn(returnIntent) + pgpMessageBuilder.buildAsync(mock(Callback::class.java)) + + verify(autocryptOpenPgpApiInteractor).getKeyMaterialForKeyId(any(OpenPgpApi::class.java), any(Long::class.java), any(String::class.java)) + verifyNoMoreInteractions(autocryptOpenPgpApiInteractor) + } + + @Test + @Throws(MessagingException::class) + fun buildEncrypt__shouldSucceed() { + val cryptoStatus = defaultCryptoStatus.copy( + cryptoMode = CryptoMode.CHOICE_ENABLED, + recipientAddresses = listOf("test@example.org")) + pgpMessageBuilder.setCryptoStatus(cryptoStatus) + + val capturedApiIntent = ArgumentCaptor.forClass(Intent::class.java) + + val returnIntent = Intent() + returnIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS) + + `when`(openPgpApi.executeApi(capturedApiIntent.capture(), any(OpenPgpDataSource::class.java), + any(OutputStream::class.java))).thenReturn(returnIntent) + + val mockCallback = mock(Callback::class.java) + pgpMessageBuilder.buildAsync(mockCallback) + + val expectedApiIntent = Intent(OpenPgpApi.ACTION_SIGN_AND_ENCRYPT) + expectedApiIntent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, TEST_KEY_ID) + expectedApiIntent.putExtra(OpenPgpApi.EXTRA_KEY_IDS, longArrayOf(TEST_KEY_ID)) + expectedApiIntent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true) + expectedApiIntent.putExtra(OpenPgpApi.EXTRA_USER_IDS, cryptoStatus.recipientAddressesAsArray) + assertIntentEqualsActionAndExtras(expectedApiIntent, capturedApiIntent.value) + + val captor = ArgumentCaptor.forClass(MimeMessage::class.java) + verify(mockCallback).onMessageBuildSuccess(captor.capture(), eq(false)) + verifyNoMoreInteractions(mockCallback) + + val message = captor.value + + Assert.assertEquals("message must be multipart/encrypted", "multipart/encrypted", message.mimeType) + + val multipart = message.body as MimeMultipart + Assert.assertEquals("multipart/encrypted must consist of two parts", 2, multipart.count.toLong()) + + val dummyBodyPart = multipart.getBodyPart(0) + Assert.assertEquals("first part must be pgp encrypted dummy part", + "application/pgp-encrypted", dummyBodyPart.contentType) + assertContentOfBodyPartEquals("content must match the supplied detached signature", + dummyBodyPart, "Version: 1") + + val encryptedBodyPart = multipart.getBodyPart(1) + Assert.assertEquals("second part must be octet-stream of encrypted data", + "application/octet-stream; name=\"encrypted.asc\"", encryptedBodyPart.contentType) + assertTrue("message body must be BinaryTempFileBody", + encryptedBodyPart.body is BinaryTempFileBody) + Assert.assertEquals(MimeUtil.ENC_7BIT, (encryptedBodyPart.body as BinaryTempFileBody).encoding) + + assertMessageHasAutocryptHeader(message, SENDER_EMAIL, false, AUTOCRYPT_KEY_MATERIAL) + } + + @Test + @Throws(MessagingException::class) + fun buildEncrypt__withInlineEnabled__shouldSucceed() { + val cryptoStatus = defaultCryptoStatus.copy( + cryptoMode = CryptoMode.CHOICE_ENABLED, + isPgpInlineModeEnabled = true, + recipientAddresses = listOf("test@example.org")) + pgpMessageBuilder.setCryptoStatus(cryptoStatus) + + val capturedApiIntent = ArgumentCaptor.forClass(Intent::class.java) + + val returnIntent = Intent() + returnIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS) + + `when`(openPgpApi.executeApi(capturedApiIntent.capture(), any(OpenPgpDataSource::class.java), + any(OutputStream::class.java))).thenReturn(returnIntent) + + val mockCallback = mock(Callback::class.java) + pgpMessageBuilder.buildAsync(mockCallback) + + val expectedApiIntent = Intent(OpenPgpApi.ACTION_SIGN_AND_ENCRYPT) + expectedApiIntent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, TEST_KEY_ID) + expectedApiIntent.putExtra(OpenPgpApi.EXTRA_KEY_IDS, longArrayOf(TEST_KEY_ID)) + expectedApiIntent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true) + expectedApiIntent.putExtra(OpenPgpApi.EXTRA_USER_IDS, cryptoStatus.recipientAddressesAsArray) + assertIntentEqualsActionAndExtras(expectedApiIntent, capturedApiIntent.value) + + val captor = ArgumentCaptor.forClass(MimeMessage::class.java) + verify(mockCallback).onMessageBuildSuccess(captor.capture(), eq(false)) + verifyNoMoreInteractions(mockCallback) + + val message = captor.value + Assert.assertEquals("text/plain", message.mimeType) + assertTrue("message body must be BinaryTempFileBody", message.body is BinaryTempFileBody) + Assert.assertEquals(MimeUtil.ENC_7BIT, (message.body as BinaryTempFileBody).encoding) + + assertMessageHasAutocryptHeader(message, SENDER_EMAIL, false, AUTOCRYPT_KEY_MATERIAL) + } + + @Test + @Throws(MessagingException::class) + fun buildSign__withInlineEnabled__shouldSucceed() { + val cryptoStatus = defaultCryptoStatus.copy( + cryptoMode = CryptoMode.SIGN_ONLY, + isPgpInlineModeEnabled = true, + recipientAddresses = listOf("test@example.org")) + + pgpMessageBuilder.setCryptoStatus(cryptoStatus) + + val capturedApiIntent = ArgumentCaptor.forClass(Intent::class.java) + + val returnIntent = Intent() + returnIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS) + + `when`(openPgpApi.executeApi(capturedApiIntent.capture(), any(OpenPgpDataSource::class.java), + any(OutputStream::class.java))).thenReturn(returnIntent) + + val mockCallback = mock(Callback::class.java) + pgpMessageBuilder.buildAsync(mockCallback) + + val expectedApiIntent = Intent(OpenPgpApi.ACTION_SIGN) + expectedApiIntent.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, TEST_KEY_ID) + expectedApiIntent.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true) + assertIntentEqualsActionAndExtras(expectedApiIntent, capturedApiIntent.value) + + val captor = ArgumentCaptor.forClass(MimeMessage::class.java) + verify(mockCallback).onMessageBuildSuccess(captor.capture(), eq(false)) + verifyNoMoreInteractions(mockCallback) + + val message = captor.value + Assert.assertEquals("message must be text/plain", "text/plain", message.mimeType) + + assertMessageHasAutocryptHeader(message, SENDER_EMAIL, false, AUTOCRYPT_KEY_MATERIAL) + } + + @Test + @Throws(MessagingException::class) + fun buildSignWithAttach__withInlineEnabled__shouldThrow() { + val cryptoStatus = defaultCryptoStatus.copy(cryptoMode = CryptoMode.SIGN_ONLY, isPgpInlineModeEnabled = true) + + pgpMessageBuilder.setCryptoStatus(cryptoStatus) + pgpMessageBuilder.setAttachments(listOf(Attachment.createAttachment(null, 0, null, true))) + + val mockCallback = mock(Callback::class.java) + pgpMessageBuilder.buildAsync(mockCallback) + + verify(mockCallback).onMessageBuildException(any()) + verifyNoMoreInteractions(mockCallback) + verifyNoMoreInteractions(openPgpApi) + } + + @Test + @Throws(MessagingException::class) + fun buildEncryptWithAttach__withInlineEnabled__shouldThrow() { + val cryptoStatus = defaultCryptoStatus.copy(cryptoMode = CryptoMode.CHOICE_ENABLED, isPgpInlineModeEnabled = true) + + pgpMessageBuilder.setCryptoStatus(cryptoStatus) + pgpMessageBuilder.setAttachments(listOf(Attachment.createAttachment(null, 0, null, true))) + + val mockCallback = mock(Callback::class.java) + pgpMessageBuilder.buildAsync(mockCallback) + + verify(mockCallback).onMessageBuildException(any()) + verifyNoMoreInteractions(mockCallback) + verifyNoMoreInteractions(openPgpApi) + } + + @Test + @Throws(MessagingException::class) + fun buildOpportunisticEncrypt__withNoKeysAndNoSignOnly__shouldNotBeSigned() { + val cryptoStatus = defaultCryptoStatus.copy(recipientAddresses = listOf("test@example.org")) + pgpMessageBuilder.setCryptoStatus(cryptoStatus) + + + val returnIntent = Intent() + returnIntent.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR) + returnIntent.putExtra(OpenPgpApi.RESULT_ERROR, + OpenPgpError(OpenPgpError.OPPORTUNISTIC_MISSING_KEYS, "Missing keys")) + + + `when`(openPgpApi.executeApi(any(Intent::class.java), any(OpenPgpDataSource::class.java), any(OutputStream::class.java))) + .thenReturn(returnIntent) + + val mockCallback = mock(Callback::class.java) + pgpMessageBuilder.buildAsync(mockCallback) + + + val captor = ArgumentCaptor.forClass(MimeMessage::class.java) + verify(mockCallback).onMessageBuildSuccess(captor.capture(), eq(false)) + verifyNoMoreInteractions(mockCallback) + + val message = captor.value + Assert.assertEquals("text/plain", message.mimeType) + } + + @Test + @Throws(MessagingException::class) + fun buildSign__withNoDetachedSignatureExtra__shouldFail() { + val cryptoStatus = defaultCryptoStatus.copy(cryptoMode = CryptoMode.SIGN_ONLY) + pgpMessageBuilder.setCryptoStatus(cryptoStatus) + + val returnIntentSigned = Intent() + returnIntentSigned.putExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_SUCCESS) + // no OpenPgpApi.EXTRA_DETACHED_SIGNATURE! + + + `when`(openPgpApi.executeApi(any(), any(), any())).thenReturn(returnIntentSigned) + val mockCallback = mock(Callback::class.java) + pgpMessageBuilder.buildAsync(mockCallback) + + + verify(mockCallback).onMessageBuildException(any()) + verifyNoMoreInteractions(mockCallback) + } + + companion object { + private val TEST_KEY_ID = 123L + private val TEST_MESSAGE_TEXT = "message text with a ☭ CCCP symbol" + private val AUTOCRYPT_KEY_MATERIAL = byteArrayOf(1, 2, 3) + private val SENDER_EMAIL = "test@example.org" + + private fun createDefaultPgpMessageBuilder(openPgpApi: OpenPgpApi, + autocryptOpenPgpApiInteractor: AutocryptOpenPgpApiInteractor): PgpMessageBuilder { + val builder = PgpMessageBuilder( + RuntimeEnvironment.application, MessageIdGenerator.getInstance(), BoundaryGenerator.getInstance(), + AutocryptOperations.getInstance(), autocryptOpenPgpApiInteractor) + builder.setOpenPgpApi(openPgpApi) + + val identity = Identity() + identity.name = "tester" + identity.email = SENDER_EMAIL + identity.description = "test identity" + identity.signatureUse = false + + builder.setSubject("subject") + .setSentDate(Date()) + .setHideTimeZone(false) + .setTo(ArrayList()) + .setCc(ArrayList()) + .setBcc(ArrayList()) + .setInReplyTo("inreplyto") + .setReferences("references") + .setRequestReadReceipt(false) + .setIdentity(identity) + .setMessageFormat(SimpleMessageFormat.TEXT) + .setText(TEST_MESSAGE_TEXT) + .setAttachments(ArrayList()) + .setSignature("signature") + .setQuoteStyle(QuoteStyle.PREFIX) + .setQuotedTextMode(QuotedTextMode.NONE) + .setQuotedText("quoted text") + .setQuotedHtmlContent(InsertableHtmlContent()) + .setReplyAfterQuote(false) + .setSignatureBeforeQuotedText(false) + .setIdentityChanged(false) + .setSignatureChanged(false) + .setCursorPosition(0) + .setMessageReference(null).isDraft = false + + return builder + } + + private fun assertContentOfBodyPartEquals(reason: String, signatureBodyPart: BodyPart, expected: ByteArray) { + try { + val bos = ByteArrayOutputStream() + signatureBodyPart.body.writeTo(bos) + Assert.assertArrayEquals(reason, expected, bos.toByteArray()) + } catch (e: IOException) { + Assert.fail() + } catch (e: MessagingException) { + Assert.fail() + } + + } + + private fun assertContentOfBodyPartEquals(reason: String, signatureBodyPart: BodyPart, expected: String) { + try { + val bos = ByteArrayOutputStream() + val inputStream = MimeUtility.decodeBody(signatureBodyPart.body) + IOUtils.copy(inputStream, bos) + Assert.assertEquals(reason, expected, String(bos.toByteArray(), Charsets.UTF_8)) + } catch (e: IOException) { + Assert.fail() + } catch (e: MessagingException) { + Assert.fail() + } + + } + + private fun assertIntentEqualsActionAndExtras(expected: Intent, actual: Intent) { + Assert.assertEquals(expected.action, actual.action) + + val expectedExtras = expected.extras + val intentExtras = actual.extras + + if (expectedExtras!!.size() != intentExtras!!.size()) { + Assert.assertEquals(expectedExtras.size().toLong(), intentExtras.size().toLong()) + } + + for (key in expectedExtras.keySet()) { + val intentExtra = intentExtras.get(key) + val expectedExtra = expectedExtras.get(key) + if (intentExtra == null) { + if (expectedExtra == null) { + continue + } + Assert.fail("found null for an expected non-null extra: $key") + } + if (intentExtra is LongArray) { + if (!Arrays.equals(intentExtra, expectedExtra as LongArray)) { + Assert.assertArrayEquals("error in $key", expectedExtra, intentExtra) + } + } else { + if (intentExtra != expectedExtra) { + Assert.assertEquals("error in $key", expectedExtra, intentExtra) + } + } + } + } + } +} diff --git a/app/ui/src/main/java/com/fsck/k9/activity/MessageCompose.java b/app/ui/src/main/java/com/fsck/k9/activity/MessageCompose.java index 6d52c9c33..e6c85da3b 100644 --- a/app/ui/src/main/java/com/fsck/k9/activity/MessageCompose.java +++ b/app/ui/src/main/java/com/fsck/k9/activity/MessageCompose.java @@ -665,7 +665,8 @@ public class MessageCompose extends K9Activity implements OnClickListener, return null; } - if (cryptoStatus.shouldUsePgpMessageBuilder()) { + boolean shouldUsePgpMessageBuilder = cryptoStatus.isOpenPgpConfigured(); + if (shouldUsePgpMessageBuilder) { SendErrorState maybeSendErrorState = cryptoStatus.getSendErrorStateOrNull(); if (maybeSendErrorState != null) { recipientPresenter.showPgpSendError(maybeSendErrorState); diff --git a/app/ui/src/main/java/com/fsck/k9/activity/compose/AttachmentPresenter.java b/app/ui/src/main/java/com/fsck/k9/activity/compose/AttachmentPresenter.java index afbc52e11..f6e6790cf 100644 --- a/app/ui/src/main/java/com/fsck/k9/activity/compose/AttachmentPresenter.java +++ b/app/ui/src/main/java/com/fsck/k9/activity/compose/AttachmentPresenter.java @@ -17,11 +17,11 @@ import android.os.Handler; import android.support.v4.app.LoaderManager; import android.support.v4.content.Loader; -import com.fsck.k9.controller.MessageReference; import com.fsck.k9.activity.compose.ComposeCryptoStatus.AttachErrorState; import com.fsck.k9.activity.loader.AttachmentContentLoader; import com.fsck.k9.activity.loader.AttachmentInfoLoader; import com.fsck.k9.activity.misc.Attachment; +import com.fsck.k9.controller.MessageReference; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mailstore.AttachmentViewInfo; import com.fsck.k9.mailstore.LocalMessage; diff --git a/app/ui/src/main/java/com/fsck/k9/activity/compose/ComposeCryptoStatus.java b/app/ui/src/main/java/com/fsck/k9/activity/compose/ComposeCryptoStatus.java deleted file mode 100644 index 085eff606..000000000 --- a/app/ui/src/main/java/com/fsck/k9/activity/compose/ComposeCryptoStatus.java +++ /dev/null @@ -1,322 +0,0 @@ -package com.fsck.k9.activity.compose; - - -import java.util.ArrayList; -import java.util.List; - -import android.app.PendingIntent; - -import com.fsck.k9.activity.compose.RecipientMvpView.CryptoSpecialModeDisplayType; -import com.fsck.k9.activity.compose.RecipientMvpView.CryptoStatusDisplayType; -import com.fsck.k9.activity.compose.RecipientPresenter.CryptoMode; -import com.fsck.k9.message.AutocryptStatusInteractor.RecipientAutocryptStatus; -import com.fsck.k9.message.AutocryptStatusInteractor.RecipientAutocryptStatusType; -import com.fsck.k9.message.CryptoStatus; -import org.openintents.openpgp.OpenPgpApiManager.OpenPgpProviderState; -import com.fsck.k9.view.RecipientSelectView.Recipient; - -/** This is an immutable object which contains all relevant metadata entered - * during email composition to apply cryptographic operations before sending - * or saving as draft. - */ -public class ComposeCryptoStatus implements CryptoStatus { - private OpenPgpProviderState openPgpProviderState; - private Long openPgpKeyId; - private String[] recipientAddresses; - private boolean enablePgpInline; - private boolean preferEncryptMutual; - private boolean isReplyToEncrypted; - private boolean encryptSubject; - private CryptoMode cryptoMode; - private RecipientAutocryptStatus recipientAutocryptStatus; - - - public Long getOpenPgpKeyId() { - return openPgpKeyId; - } - - CryptoStatusDisplayType getCryptoStatusDisplayType() { - switch (openPgpProviderState) { - case UNCONFIGURED: - return CryptoStatusDisplayType.UNCONFIGURED; - case UNINITIALIZED: - return CryptoStatusDisplayType.UNINITIALIZED; - case ERROR: - case UI_REQUIRED: - return CryptoStatusDisplayType.ERROR; - case OK: - // provider status is ok -> return value is based on cryptoMode - break; - default: - throw new AssertionError("all CryptoProviderStates must be handled!"); - } - - if (recipientAutocryptStatus == null) { - throw new IllegalStateException("Display type must be obtained from provider!"); - } - - RecipientAutocryptStatusType recipientAutocryptStatusType = recipientAutocryptStatus.type; - - if (recipientAutocryptStatusType == RecipientAutocryptStatusType.ERROR) { - return CryptoStatusDisplayType.ERROR; - } - - if (isEncryptionEnabled()) { - if (!recipientAutocryptStatusType.canEncrypt()) { - return CryptoStatusDisplayType.ENABLED_ERROR; - } else if (recipientAutocryptStatusType.isConfirmed()) { - return CryptoStatusDisplayType.ENABLED_TRUSTED; - } else { - return CryptoStatusDisplayType.ENABLED; - } - } else if (isSigningEnabled()) { - return CryptoStatusDisplayType.SIGN_ONLY; - } else if (recipientAutocryptStatusType.canEncrypt()) { - return CryptoStatusDisplayType.AVAILABLE; - } else { - return CryptoStatusDisplayType.UNAVAILABLE; - } - } - - CryptoSpecialModeDisplayType getCryptoSpecialModeDisplayType() { - if (openPgpProviderState != OpenPgpProviderState.OK) { - return CryptoSpecialModeDisplayType.NONE; - } - - if (isSignOnly() && isPgpInlineModeEnabled()) { - return CryptoSpecialModeDisplayType.SIGN_ONLY_PGP_INLINE; - } - - if (isSignOnly()) { - return CryptoSpecialModeDisplayType.SIGN_ONLY; - } - - if (allRecipientsCanEncrypt() && isPgpInlineModeEnabled()) { - return CryptoSpecialModeDisplayType.PGP_INLINE; - } - - return CryptoSpecialModeDisplayType.NONE; - } - - public boolean shouldUsePgpMessageBuilder() { - // CryptoProviderState.ERROR will be handled as an actual error, see SendErrorState - return openPgpProviderState != OpenPgpProviderState.UNCONFIGURED; - } - - public boolean isEncryptionEnabled() { - if (openPgpProviderState == OpenPgpProviderState.UNCONFIGURED) { - return false; - } - - boolean isExplicitlyEnabled = (cryptoMode == CryptoMode.CHOICE_ENABLED); - boolean isMutualAndNotDisabled = (cryptoMode != CryptoMode.CHOICE_DISABLED && canEncryptAndIsMutualDefault()); - boolean isReplyAndNotDisabled = (cryptoMode != CryptoMode.CHOICE_DISABLED && isReplyToEncrypted()); - return isExplicitlyEnabled || isMutualAndNotDisabled || isReplyAndNotDisabled; - } - - public boolean isSignOnly() { - return cryptoMode == CryptoMode.SIGN_ONLY; - } - - public boolean isSigningEnabled() { - return cryptoMode == CryptoMode.SIGN_ONLY || isEncryptionEnabled(); - } - - public boolean isPgpInlineModeEnabled() { - return enablePgpInline; - } - - public boolean isProviderStateOk() { - return openPgpProviderState == OpenPgpProviderState.OK; - } - - boolean allRecipientsCanEncrypt() { - return recipientAutocryptStatus != null && recipientAutocryptStatus.type.canEncrypt(); - } - - public boolean isUserChoice() { - return cryptoMode != CryptoMode.NO_CHOICE; - } - - public String[] getRecipientAddresses() { - return recipientAddresses; - } - - public boolean hasRecipients() { - return recipientAddresses.length > 0; - } - - public boolean isSenderPreferEncryptMutual() { - return preferEncryptMutual; - } - - private boolean isRecipientsPreferEncryptMutual() { - return recipientAutocryptStatus.type.isMutual(); - } - - public boolean isReplyToEncrypted() { - return isReplyToEncrypted; - } - - boolean canEncryptAndIsMutualDefault() { - return allRecipientsCanEncrypt() && isSenderPreferEncryptMutual() && isRecipientsPreferEncryptMutual(); - } - - boolean hasAutocryptPendingIntent() { - return recipientAutocryptStatus.hasPendingIntent(); - } - - PendingIntent getAutocryptPendingIntent() { - return recipientAutocryptStatus.intent; - } - - public boolean isEncryptSubject() { - return encryptSubject; - } - - public static class ComposeCryptoStatusBuilder { - - private OpenPgpProviderState openPgpProviderState; - private CryptoMode cryptoMode; - private Long openPgpKeyId; - private List recipients; - private Boolean enablePgpInline; - private Boolean preferEncryptMutual; - private Boolean isReplyToEncrypted; - private Boolean encryptSubject; - - public ComposeCryptoStatusBuilder setOpenPgpProviderState(OpenPgpProviderState openPgpProviderState) { - this.openPgpProviderState = openPgpProviderState; - return this; - } - - public ComposeCryptoStatusBuilder setCryptoMode(CryptoMode cryptoMode) { - this.cryptoMode = cryptoMode; - return this; - } - - public ComposeCryptoStatusBuilder setOpenPgpKeyId(Long openPgpKeyId) { - this.openPgpKeyId = openPgpKeyId; - return this; - } - - public ComposeCryptoStatusBuilder setRecipients(List recipients) { - this.recipients = recipients; - return this; - } - - public ComposeCryptoStatusBuilder setEnablePgpInline(boolean cryptoEnableCompat) { - this.enablePgpInline = cryptoEnableCompat; - return this; - } - - public ComposeCryptoStatusBuilder setPreferEncryptMutual(boolean preferEncryptMutual) { - this.preferEncryptMutual = preferEncryptMutual; - return this; - } - - public ComposeCryptoStatusBuilder setIsReplyToEncrypted(boolean isReplyToEncrypted) { - this.isReplyToEncrypted = isReplyToEncrypted; - return this; - } - - public ComposeCryptoStatusBuilder setEncryptSubject(boolean encryptSubject) { - this.encryptSubject = encryptSubject; - return this; - } - - public ComposeCryptoStatus build() { - if (openPgpProviderState == null) { - throw new AssertionError("cryptoProviderState must be set!"); - } - if (cryptoMode == null) { - throw new AssertionError("crypto mode must be set!"); - } - if (recipients == null) { - throw new AssertionError("recipients must be set!"); - } - if (enablePgpInline == null) { - throw new AssertionError("enablePgpInline must be set!"); - } - if (preferEncryptMutual == null) { - throw new AssertionError("preferEncryptMutual must be set!"); - } - if (isReplyToEncrypted == null) { - throw new AssertionError("isReplyToEncrypted must be set!"); - } - if (encryptSubject == null) { - throw new AssertionError("encryptSubject must be set!"); - } - - ArrayList recipientAddresses = new ArrayList<>(); - for (Recipient recipient : recipients) { - recipientAddresses.add(recipient.address.getAddress()); - } - - ComposeCryptoStatus result = new ComposeCryptoStatus(); - result.openPgpProviderState = openPgpProviderState; - result.cryptoMode = cryptoMode; - result.recipientAddresses = recipientAddresses.toArray(new String[0]); - result.openPgpKeyId = openPgpKeyId; - result.isReplyToEncrypted = isReplyToEncrypted; - result.enablePgpInline = enablePgpInline; - result.preferEncryptMutual = preferEncryptMutual; - result.encryptSubject = encryptSubject; - return result; - } - } - - ComposeCryptoStatus withRecipientAutocryptStatus(RecipientAutocryptStatus recipientAutocryptStatusType) { - ComposeCryptoStatus result = new ComposeCryptoStatus(); - result.openPgpProviderState = openPgpProviderState; - result.cryptoMode = cryptoMode; - result.recipientAddresses = recipientAddresses; - result.isReplyToEncrypted = isReplyToEncrypted; - result.openPgpKeyId = openPgpKeyId; - result.enablePgpInline = enablePgpInline; - result.preferEncryptMutual = preferEncryptMutual; - result.encryptSubject = encryptSubject; - result.recipientAutocryptStatus = recipientAutocryptStatusType; - return result; - } - - public enum SendErrorState { - PROVIDER_ERROR, - KEY_CONFIG_ERROR, - ENABLED_ERROR - } - - public SendErrorState getSendErrorStateOrNull() { - if (openPgpProviderState != OpenPgpProviderState.OK) { - // TODO: be more specific about this error - return SendErrorState.PROVIDER_ERROR; - } - - if (openPgpKeyId == null && (isEncryptionEnabled() || isSignOnly())) { - return SendErrorState.KEY_CONFIG_ERROR; - } - - if (isEncryptionEnabled() && !allRecipientsCanEncrypt()) { - return SendErrorState.ENABLED_ERROR; - } - - return null; - } - - enum AttachErrorState { - IS_INLINE - } - - AttachErrorState getAttachErrorStateOrNull() { - if (openPgpProviderState == OpenPgpProviderState.UNCONFIGURED) { - return null; - } - - if (enablePgpInline) { - return AttachErrorState.IS_INLINE; - } - - return null; - } - -} diff --git a/app/ui/src/main/java/com/fsck/k9/activity/compose/ComposeCryptoStatus.kt b/app/ui/src/main/java/com/fsck/k9/activity/compose/ComposeCryptoStatus.kt new file mode 100644 index 000000000..a1d804c12 --- /dev/null +++ b/app/ui/src/main/java/com/fsck/k9/activity/compose/ComposeCryptoStatus.kt @@ -0,0 +1,161 @@ +package com.fsck.k9.activity.compose + + +import com.fsck.k9.activity.compose.RecipientMvpView.CryptoSpecialModeDisplayType +import com.fsck.k9.activity.compose.RecipientMvpView.CryptoStatusDisplayType +import com.fsck.k9.activity.compose.RecipientPresenter.CryptoMode +import com.fsck.k9.message.AutocryptStatusInteractor +import com.fsck.k9.message.AutocryptStatusInteractor.RecipientAutocryptStatus +import com.fsck.k9.message.CryptoStatus +import com.fsck.k9.view.RecipientSelectView.Recipient +import org.openintents.openpgp.OpenPgpApiManager +import org.openintents.openpgp.OpenPgpApiManager.OpenPgpProviderState + +/** This is an immutable object which contains all relevant metadata entered + * during email composition to apply cryptographic operations before sending + * or saving as draft. + */ +data class ComposeCryptoStatus(private val openPgpProviderState: OpenPgpProviderState, + private val cryptoMode: CryptoMode, + override val openPgpKeyId: Long?, + override val isPgpInlineModeEnabled: Boolean, + override val isSenderPreferEncryptMutual: Boolean, + override val isReplyToEncrypted: Boolean, + override val isEncryptSubject: Boolean, + val recipientAddresses: List, + private val recipientAutocryptStatus: RecipientAutocryptStatus? = null) : CryptoStatus { + + constructor(openPgpProviderState: OpenPgpProviderState, + openPgpKeyId: Long?, + recipientAddresses: List, + isPgpInlineModeEnabled: Boolean, + isSenderPreferEncryptMutual: Boolean, + isReplyToEncrypted: Boolean, + isEncryptSubject: Boolean, + cryptoMode: CryptoMode) : this( + openPgpProviderState, cryptoMode, + openPgpKeyId, + isPgpInlineModeEnabled, isSenderPreferEncryptMutual, isReplyToEncrypted, isEncryptSubject, recipientAddresses.map { it.address.address }) + + private val recipientAutocryptStatusType = recipientAutocryptStatus?.type + private val isRecipientsPreferEncryptMutual = recipientAutocryptStatus?.type?.isMutual ?: false + + private val isExplicitlyEnabled = cryptoMode == CryptoMode.CHOICE_ENABLED + private val isMutualAndNotDisabled = cryptoMode != CryptoMode.CHOICE_DISABLED && canEncryptAndIsMutualDefault() + private val isReplyAndNotDisabled = cryptoMode != CryptoMode.CHOICE_DISABLED && isReplyToEncrypted + + val isOpenPgpConfigured = openPgpProviderState != OpenPgpProviderState.UNCONFIGURED + + override val isSignOnly = cryptoMode == CryptoMode.SIGN_ONLY + + override val isEncryptionEnabled = when { + openPgpProviderState == OpenPgpProviderState.UNCONFIGURED -> false + isSignOnly -> false + isExplicitlyEnabled -> true + isMutualAndNotDisabled -> true + isReplyAndNotDisabled -> true + else -> false + } + + override fun isProviderStateOk() = openPgpProviderState == OpenPgpProviderState.OK + + override fun isUserChoice() = cryptoMode != CryptoMode.NO_CHOICE + override fun isSigningEnabled() = cryptoMode == CryptoMode.SIGN_ONLY || isEncryptionEnabled + val recipientAddressesAsArray = recipientAddresses.toTypedArray() + + private val displayTypeFromProviderError = when (openPgpProviderState) { + OpenPgpApiManager.OpenPgpProviderState.OK -> null + OpenPgpApiManager.OpenPgpProviderState.UNCONFIGURED -> CryptoStatusDisplayType.UNCONFIGURED + OpenPgpApiManager.OpenPgpProviderState.UNINITIALIZED -> CryptoStatusDisplayType.UNINITIALIZED + OpenPgpApiManager.OpenPgpProviderState.ERROR, OpenPgpApiManager.OpenPgpProviderState.UI_REQUIRED -> CryptoStatusDisplayType.ERROR + } + + private val displayTypeFromAutocryptError = when (recipientAutocryptStatusType) { + null, AutocryptStatusInteractor.RecipientAutocryptStatusType.ERROR -> CryptoStatusDisplayType.ERROR + else -> null + } + + private val displayTypeFromEnabledAutocryptStatus = when { + !isEncryptionEnabled -> null + recipientAutocryptStatusType == null -> CryptoStatusDisplayType.ERROR + !recipientAutocryptStatusType.canEncrypt() -> CryptoStatusDisplayType.ENABLED_ERROR + recipientAutocryptStatusType.isConfirmed -> CryptoStatusDisplayType.ENABLED_TRUSTED + else -> CryptoStatusDisplayType.ENABLED + } + + private val displayTypeFromSignOnly = when { + isSignOnly -> CryptoStatusDisplayType.SIGN_ONLY + else -> null + } + + private val displayTypeFromEncryptionAvailable = when { + recipientAutocryptStatusType?.canEncrypt() == true -> CryptoStatusDisplayType.AVAILABLE + else -> null + } + + val displayType = + displayTypeFromProviderError + ?: displayTypeFromAutocryptError + ?: displayTypeFromEnabledAutocryptStatus + ?: displayTypeFromSignOnly + ?: displayTypeFromEncryptionAvailable + ?: CryptoStatusDisplayType.UNAVAILABLE + + val specialModeDisplayType = when { + openPgpProviderState != OpenPgpProviderState.OK -> CryptoSpecialModeDisplayType.NONE + isSignOnly && isPgpInlineModeEnabled -> CryptoSpecialModeDisplayType.SIGN_ONLY_PGP_INLINE + isSignOnly -> CryptoSpecialModeDisplayType.SIGN_ONLY + allRecipientsCanEncrypt() && isPgpInlineModeEnabled -> CryptoSpecialModeDisplayType.PGP_INLINE + else -> CryptoSpecialModeDisplayType.NONE + } + + val autocryptPendingIntent = recipientAutocryptStatus?.intent + + val sendErrorStateOrNull = when { + openPgpProviderState != OpenPgpProviderState.OK -> SendErrorState.PROVIDER_ERROR + openPgpKeyId == null && (isEncryptionEnabled || isSignOnly) -> SendErrorState.KEY_CONFIG_ERROR + isEncryptionEnabled && !allRecipientsCanEncrypt() -> SendErrorState.ENABLED_ERROR + else -> null + } + + val attachErrorStateOrNull = when { + openPgpProviderState == OpenPgpProviderState.UNCONFIGURED -> null + isPgpInlineModeEnabled -> AttachErrorState.IS_INLINE + else -> null + } + + fun allRecipientsCanEncrypt() = recipientAutocryptStatus?.type?.canEncrypt() == true + + fun canEncryptAndIsMutualDefault() = allRecipientsCanEncrypt() && isSenderPreferEncryptMutual && isRecipientsPreferEncryptMutual + + fun hasAutocryptPendingIntent() = recipientAutocryptStatus?.hasPendingIntent() == true + + override fun hasRecipients(): Boolean { + return recipientAddresses.isNotEmpty() + } + + override fun getRecipientAddresses() = recipientAddresses.toTypedArray() + + fun withRecipientAutocryptStatus(recipientAutocryptStatusType: RecipientAutocryptStatus) = ComposeCryptoStatus( + openPgpProviderState = openPgpProviderState, + cryptoMode = cryptoMode, + openPgpKeyId = openPgpKeyId, + isPgpInlineModeEnabled = isPgpInlineModeEnabled, + isSenderPreferEncryptMutual = isSenderPreferEncryptMutual, + isReplyToEncrypted = isReplyToEncrypted, + isEncryptSubject = isEncryptSubject, + recipientAddresses = recipientAddresses, + recipientAutocryptStatus = recipientAutocryptStatusType + ) + + enum class SendErrorState { + PROVIDER_ERROR, + KEY_CONFIG_ERROR, + ENABLED_ERROR + } + + enum class AttachErrorState { + IS_INLINE + } + +} diff --git a/app/ui/src/main/java/com/fsck/k9/activity/compose/RecipientPresenter.java b/app/ui/src/main/java/com/fsck/k9/activity/compose/RecipientPresenter.java index f0c0a3791..cd43b8e1b 100644 --- a/app/ui/src/main/java/com/fsck/k9/activity/compose/RecipientPresenter.java +++ b/app/ui/src/main/java/com/fsck/k9/activity/compose/RecipientPresenter.java @@ -22,9 +22,7 @@ import android.view.Menu; import com.fsck.k9.Account; import com.fsck.k9.Identity; import com.fsck.k9.K9; -import com.fsck.k9.ui.R; import com.fsck.k9.activity.compose.ComposeCryptoStatus.AttachErrorState; -import com.fsck.k9.activity.compose.ComposeCryptoStatus.ComposeCryptoStatusBuilder; import com.fsck.k9.activity.compose.ComposeCryptoStatus.SendErrorState; import com.fsck.k9.activity.compose.RecipientMvpView.CryptoStatusDisplayType; import com.fsck.k9.autocrypt.AutocryptDraftStateHeader; @@ -44,6 +42,7 @@ import com.fsck.k9.message.ComposePgpEnableByDefaultDecider; import com.fsck.k9.message.ComposePgpInlineDecider; import com.fsck.k9.message.MessageBuilder; import com.fsck.k9.message.PgpMessageBuilder; +import com.fsck.k9.ui.R; import com.fsck.k9.view.RecipientSelectView.Recipient; import org.openintents.openpgp.OpenPgpApiManager; import org.openintents.openpgp.OpenPgpApiManager.OpenPgpApiManagerCallback; @@ -423,16 +422,15 @@ public class RecipientPresenter { accountCryptoKey = null; } - final ComposeCryptoStatus composeCryptoStatus = new ComposeCryptoStatusBuilder() - .setOpenPgpProviderState(openPgpProviderState) - .setCryptoMode(currentCryptoMode) - .setEnablePgpInline(cryptoEnablePgpInline) - .setPreferEncryptMutual(account.getAutocryptPreferEncryptMutual()) - .setIsReplyToEncrypted(isReplyToEncryptedMessage) - .setEncryptSubject(account.getOpenPgpEncryptSubject()) - .setRecipients(getAllRecipients()) - .setOpenPgpKeyId(accountCryptoKey) - .build(); + final ComposeCryptoStatus composeCryptoStatus = new ComposeCryptoStatus( + openPgpProviderState, + accountCryptoKey, + getAllRecipients(), + cryptoEnablePgpInline, + account.getAutocryptPreferEncryptMutual(), + isReplyToEncryptedMessage, + account.getOpenPgpEncryptSubject(), + currentCryptoMode); if (openPgpProviderState != OpenPgpProviderState.OK) { cachedCryptoStatus = composeCryptoStatus; @@ -440,7 +438,7 @@ public class RecipientPresenter { return; } - final String[] recipientAddresses = composeCryptoStatus.getRecipientAddresses(); + final String[] recipientAddresses = composeCryptoStatus.getRecipientAddressesAsArray(); new AsyncTask() { @Override @@ -472,9 +470,9 @@ public class RecipientPresenter { recipientMvpView.setRecipientTokensShowCryptoEnabled(cachedCryptoStatus.isEncryptionEnabled()); - CryptoStatusDisplayType cryptoStatusDisplayType = cachedCryptoStatus.getCryptoStatusDisplayType(); + CryptoStatusDisplayType cryptoStatusDisplayType = cachedCryptoStatus.getDisplayType(); recipientMvpView.showCryptoStatus(cryptoStatusDisplayType); - recipientMvpView.showCryptoSpecialMode(cachedCryptoStatus.getCryptoSpecialModeDisplayType()); + recipientMvpView.showCryptoSpecialMode(cachedCryptoStatus.getSpecialModeDisplayType()); } @Nullable diff --git a/app/ui/src/test/java/com/fsck/k9/activity/compose/RecipientPresenterTest.java b/app/ui/src/test/java/com/fsck/k9/activity/compose/RecipientPresenterTest.java index 56579b024..7c1ead856 100644 --- a/app/ui/src/test/java/com/fsck/k9/activity/compose/RecipientPresenterTest.java +++ b/app/ui/src/test/java/com/fsck/k9/activity/compose/RecipientPresenterTest.java @@ -150,11 +150,11 @@ public class RecipientPresenterTest extends K9RobolectricTest { ComposeCryptoStatus status = recipientPresenter.getCurrentCachedCryptoStatus(); - assertEquals(CryptoStatusDisplayType.UNCONFIGURED, status.getCryptoStatusDisplayType()); - assertEquals(CryptoSpecialModeDisplayType.NONE, status.getCryptoSpecialModeDisplayType()); + assertEquals(CryptoStatusDisplayType.UNCONFIGURED, status.getDisplayType()); + assertEquals(CryptoSpecialModeDisplayType.NONE, status.getSpecialModeDisplayType()); assertNull(status.getAttachErrorStateOrNull()); assertFalse(status.isProviderStateOk()); - assertFalse(status.shouldUsePgpMessageBuilder()); + assertFalse(status.isOpenPgpConfigured()); } @Test @@ -163,9 +163,9 @@ public class RecipientPresenterTest extends K9RobolectricTest { ComposeCryptoStatus status = recipientPresenter.getCurrentCachedCryptoStatus(); - assertEquals(CryptoStatusDisplayType.UNAVAILABLE, status.getCryptoStatusDisplayType()); + assertEquals(CryptoStatusDisplayType.UNAVAILABLE, status.getDisplayType()); assertTrue(status.isProviderStateOk()); - assertTrue(status.shouldUsePgpMessageBuilder()); + assertTrue(status.isOpenPgpConfigured()); } @Test @@ -176,9 +176,9 @@ public class RecipientPresenterTest extends K9RobolectricTest { ComposeCryptoStatus status = recipientPresenter.getCurrentCachedCryptoStatus(); - assertEquals(CryptoStatusDisplayType.AVAILABLE, status.getCryptoStatusDisplayType()); + assertEquals(CryptoStatusDisplayType.AVAILABLE, status.getDisplayType()); assertTrue(status.isProviderStateOk()); - assertTrue(status.shouldUsePgpMessageBuilder()); + assertTrue(status.isOpenPgpConfigured()); } @Test @@ -189,9 +189,9 @@ public class RecipientPresenterTest extends K9RobolectricTest { ComposeCryptoStatus status = recipientPresenter.getCurrentCachedCryptoStatus(); - assertEquals(CryptoStatusDisplayType.AVAILABLE, status.getCryptoStatusDisplayType()); + assertEquals(CryptoStatusDisplayType.AVAILABLE, status.getDisplayType()); assertTrue(status.isProviderStateOk()); - assertTrue(status.shouldUsePgpMessageBuilder()); + assertTrue(status.isOpenPgpConfigured()); } @Test @@ -202,9 +202,9 @@ public class RecipientPresenterTest extends K9RobolectricTest { ComposeCryptoStatus status = recipientPresenter.getCurrentCachedCryptoStatus(); - assertEquals(CryptoStatusDisplayType.UNAVAILABLE, status.getCryptoStatusDisplayType()); + assertEquals(CryptoStatusDisplayType.UNAVAILABLE, status.getDisplayType()); assertTrue(status.isProviderStateOk()); - assertTrue(status.shouldUsePgpMessageBuilder()); + assertTrue(status.isOpenPgpConfigured()); } @Test @@ -217,9 +217,9 @@ public class RecipientPresenterTest extends K9RobolectricTest { runBackgroundTask(); ComposeCryptoStatus status = recipientPresenter.getCurrentCachedCryptoStatus(); - assertEquals(CryptoStatusDisplayType.ENABLED_ERROR, status.getCryptoStatusDisplayType()); + assertEquals(CryptoStatusDisplayType.ENABLED_ERROR, status.getDisplayType()); assertTrue(status.isProviderStateOk()); - assertTrue(status.shouldUsePgpMessageBuilder()); + assertTrue(status.isOpenPgpConfigured()); } @Test @@ -232,9 +232,9 @@ public class RecipientPresenterTest extends K9RobolectricTest { runBackgroundTask(); ComposeCryptoStatus status = recipientPresenter.getCurrentCachedCryptoStatus(); - assertEquals(CryptoStatusDisplayType.AVAILABLE, status.getCryptoStatusDisplayType()); + assertEquals(CryptoStatusDisplayType.AVAILABLE, status.getDisplayType()); assertTrue(status.isProviderStateOk()); - assertTrue(status.shouldUsePgpMessageBuilder()); + assertTrue(status.isOpenPgpConfigured()); } @Test @@ -247,9 +247,9 @@ public class RecipientPresenterTest extends K9RobolectricTest { runBackgroundTask(); ComposeCryptoStatus status = recipientPresenter.getCurrentCachedCryptoStatus(); - assertEquals(CryptoStatusDisplayType.ENABLED, status.getCryptoStatusDisplayType()); + assertEquals(CryptoStatusDisplayType.ENABLED, status.getDisplayType()); assertTrue(status.isProviderStateOk()); - assertTrue(status.shouldUsePgpMessageBuilder()); + assertTrue(status.isOpenPgpConfigured()); } @Test @@ -260,7 +260,7 @@ public class RecipientPresenterTest extends K9RobolectricTest { runBackgroundTask(); ComposeCryptoStatus status = recipientPresenter.getCurrentCachedCryptoStatus(); - assertEquals(CryptoStatusDisplayType.SIGN_ONLY, status.getCryptoStatusDisplayType()); + assertEquals(CryptoStatusDisplayType.SIGN_ONLY, status.getDisplayType()); assertTrue(status.isProviderStateOk()); assertTrue(status.isSigningEnabled()); assertTrue(status.isSignOnly()); @@ -274,7 +274,7 @@ public class RecipientPresenterTest extends K9RobolectricTest { runBackgroundTask(); ComposeCryptoStatus status = recipientPresenter.getCurrentCachedCryptoStatus(); - assertEquals(CryptoStatusDisplayType.UNAVAILABLE, status.getCryptoStatusDisplayType()); + assertEquals(CryptoStatusDisplayType.UNAVAILABLE, status.getDisplayType()); assertTrue(status.isProviderStateOk()); assertTrue(status.isPgpInlineModeEnabled()); }