diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MessageExtractor.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MessageExtractor.java index 009ecb9ba..ec15f913a 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MessageExtractor.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MessageExtractor.java @@ -162,6 +162,11 @@ public class MessageExtractor { Alternative alternative = new Alternative(text, html); outputViewableParts.add(alternative); } + } else if (isSameMimeType(part.getMimeType(), "multipart/signed")) { + if (multipart.getCount() > 0) { + BodyPart bodyPart = multipart.getBodyPart(0); + findViewablesAndAttachments(bodyPart, outputViewableParts, outputNonViewableParts); + } } else { // For all other multipart parts we recurse to grab all viewable children. for (Part bodyPart : multipart.getBodyParts()) { diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java index f5e5725ec..2b5a90667 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeBodyPart.java @@ -30,10 +30,10 @@ public class MimeBodyPart extends BodyPart { this(body, null); } - public MimeBodyPart(Body body, String mimeType) throws MessagingException { + public MimeBodyPart(Body body, String contentType) throws MessagingException { mHeader = new MimeHeader(); - if (mimeType != null) { - addHeader(MimeHeader.HEADER_CONTENT_TYPE, mimeType); + if (contentType != null) { + addHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType); } MimeMessageHelper.setBody(this, body); } 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 54787eb63..954cf6208 100644 --- a/k9mail/src/main/java/com/fsck/k9/activity/MessageLoaderHelper.java +++ b/k9mail/src/main/java/com/fsck/k9/activity/MessageLoaderHelper.java @@ -81,6 +81,7 @@ public class MessageLoaderHelper { private LoaderManager loaderManager; @Nullable // make this explicitly nullable, make sure to cancel/ignore any operation if this is null private MessageLoaderCallbacks callback; + private final boolean processSignedOnly; // transient state @@ -100,6 +101,8 @@ public class MessageLoaderHelper { this.loaderManager = loaderManager; this.fragmentManager = fragmentManager; this.callback = callback; + + processSignedOnly = K9.getOpenPgpSupportSignOnly(); } @@ -276,7 +279,7 @@ public class MessageLoaderHelper { retainCryptoHelperFragment.setData(messageCryptoHelper); } messageCryptoHelper.asyncStartOrResumeProcessingMessage( - localMessage, messageCryptoCallback, cachedDecryptionResult); + localMessage, messageCryptoCallback, cachedDecryptionResult, processSignedOnly); } private void cancelAndClearCryptoOperation() { diff --git a/k9mail/src/main/java/com/fsck/k9/crypto/MessageDecryptVerifier.java b/k9mail/src/main/java/com/fsck/k9/crypto/MessageCryptoStructureDetector.java similarity index 76% rename from k9mail/src/main/java/com/fsck/k9/crypto/MessageDecryptVerifier.java rename to k9mail/src/main/java/com/fsck/k9/crypto/MessageCryptoStructureDetector.java index cec1e0581..974811957 100644 --- a/k9mail/src/main/java/com/fsck/k9/crypto/MessageDecryptVerifier.java +++ b/k9mail/src/main/java/com/fsck/k9/crypto/MessageCryptoStructureDetector.java @@ -18,6 +18,7 @@ import com.fsck.k9.mail.Multipart; import com.fsck.k9.mail.Part; import com.fsck.k9.mail.internet.MessageExtractor; import com.fsck.k9.mail.internet.MimeBodyPart; +import com.fsck.k9.mail.internet.MimeMultipart; import com.fsck.k9.mail.internet.MimeUtility; import com.fsck.k9.mailstore.CryptoResultAnnotation; import com.fsck.k9.ui.crypto.MessageCryptoAnnotations; @@ -25,7 +26,7 @@ import com.fsck.k9.ui.crypto.MessageCryptoAnnotations; import static com.fsck.k9.mail.internet.MimeUtility.isSameMimeType; -public class MessageDecryptVerifier { +public class MessageCryptoStructureDetector { private static final String MULTIPART_ENCRYPTED = "multipart/encrypted"; private static final String MULTIPART_SIGNED = "multipart/signed"; private static final String PROTOCOL_PARAMETER = "protocol"; @@ -109,7 +110,7 @@ public class MessageDecryptVerifier { return null; } - public static List findEncryptedParts(Part startPart) { + public static List findMultipartEncryptedParts(Part startPart) { List encryptedParts = new ArrayList<>(); Stack partsToCheck = new Stack<>(); partsToCheck.push(startPart); @@ -135,7 +136,7 @@ public class MessageDecryptVerifier { return encryptedParts; } - public static List findSignedParts(Part startPart, MessageCryptoAnnotations messageCryptoAnnotations) { + public static List findMultipartSignedParts(Part startPart, MessageCryptoAnnotations messageCryptoAnnotations) { List signedParts = new ArrayList<>(); Stack partsToCheck = new Stack<>(); partsToCheck.push(startPart); @@ -214,24 +215,59 @@ public class MessageDecryptVerifier { } private static boolean isPartMultipartSigned(Part part) { - return isSameMimeType(part.getMimeType(), MULTIPART_SIGNED); + if (!isSameMimeType(part.getMimeType(), MULTIPART_SIGNED)) { + return false; + } + if (! (part.getBody() instanceof MimeMultipart)) { + return false; + } + MimeMultipart mimeMultipart = (MimeMultipart) part.getBody(); + if (mimeMultipart.getCount() != 2) { + return false; + } + + String protocolParameter = MimeUtility.getHeaderParameter(part.getContentType(), PROTOCOL_PARAMETER); + + boolean dataUnavailable = protocolParameter == null && mimeMultipart.getBodyPart(0).getBody() == null; + boolean protocolMatches = isSameMimeType(protocolParameter, mimeMultipart.getBodyPart(1).getMimeType()); + return dataUnavailable || protocolMatches; } private static boolean isPartMultipartEncrypted(Part part) { - return isSameMimeType(part.getMimeType(), MULTIPART_ENCRYPTED); + if (!isSameMimeType(part.getMimeType(), MULTIPART_ENCRYPTED)) { + return false; + } + if (! (part.getBody() instanceof MimeMultipart)) { + return false; + } + MimeMultipart mimeMultipart = (MimeMultipart) part.getBody(); + if (mimeMultipart.getCount() != 2) { + return false; + } + + String protocolParameter = MimeUtility.getHeaderParameter(part.getContentType(), PROTOCOL_PARAMETER); + + boolean dataUnavailable = protocolParameter == null && mimeMultipart.getBodyPart(1).getBody() == null; + boolean protocolMatches = isSameMimeType(protocolParameter, mimeMultipart.getBodyPart(0).getMimeType()); + return dataUnavailable || protocolMatches; } - // TODO also guess by mime-type of contained part? - public static boolean isPgpMimeEncryptedOrSignedPart(Part part) { - String contentType = part.getContentType(); - String protocolParameter = MimeUtility.getHeaderParameter(contentType, PROTOCOL_PARAMETER); + public static boolean isMultipartEncryptedOpenPgpProtocol(Part part) { + if (!isSameMimeType(part.getMimeType(), MULTIPART_ENCRYPTED)) { + throw new IllegalArgumentException("Part is not multipart/encrypted!"); + } - boolean isPgpEncrypted = isSameMimeType(part.getMimeType(), MULTIPART_ENCRYPTED) && - APPLICATION_PGP_ENCRYPTED.equalsIgnoreCase(protocolParameter); - boolean isPgpSigned = isSameMimeType(part.getMimeType(), MULTIPART_SIGNED) && - APPLICATION_PGP_SIGNATURE.equalsIgnoreCase(protocolParameter); + String protocolParameter = MimeUtility.getHeaderParameter(part.getContentType(), PROTOCOL_PARAMETER); + return APPLICATION_PGP_ENCRYPTED.equalsIgnoreCase(protocolParameter); + } - return isPgpEncrypted || isPgpSigned; + public static boolean isMultipartSignedOpenPgpProtocol(Part part) { + if (!isSameMimeType(part.getMimeType(), MULTIPART_SIGNED)) { + throw new IllegalArgumentException("Part is not multipart/signed!"); + } + + String protocolParameter = MimeUtility.getHeaderParameter(part.getContentType(), PROTOCOL_PARAMETER); + return APPLICATION_PGP_SIGNATURE.equalsIgnoreCase(protocolParameter); } @VisibleForTesting diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/MessageViewInfoExtractor.java b/k9mail/src/main/java/com/fsck/k9/mailstore/MessageViewInfoExtractor.java index a54aaa5e1..8d318ca69 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/MessageViewInfoExtractor.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/MessageViewInfoExtractor.java @@ -28,6 +28,7 @@ import com.fsck.k9.message.html.HtmlProcessor; import com.fsck.k9.ui.crypto.MessageCryptoAnnotations; import com.fsck.k9.ui.crypto.MessageCryptoSplitter; import com.fsck.k9.ui.crypto.MessageCryptoSplitter.CryptoMessageParts; +import org.openintents.openpgp.util.OpenPgpUtils; import timber.log.Timber; import static com.fsck.k9.mail.internet.MimeUtility.getHeaderParameter; @@ -227,7 +228,7 @@ public class MessageViewInfoExtractor { Part part = ((Textual)viewable).getPart(); addHtmlDivider(html, part, prependDivider); - String t = MessageExtractor.getTextFromPart(part); + String t = getTextFromPart(part); if (t == null) { t = ""; } else if (viewable instanceof Flowed) { @@ -264,7 +265,7 @@ public class MessageViewInfoExtractor { Part part = ((Textual)viewable).getPart(); addTextDivider(text, part, prependDivider); - String t = MessageExtractor.getTextFromPart(part); + String t = getTextFromPart(part); if (t == null) { t = ""; } else if (viewable instanceof Html) { @@ -315,6 +316,17 @@ public class MessageViewInfoExtractor { } } + private String getTextFromPart(Part part) { + String textFromPart = MessageExtractor.getTextFromPart(part); + + String extractedClearsignedMessage = OpenPgpUtils.extractClearsignedMessage(textFromPart); + if (extractedClearsignedMessage != null) { + textFromPart = extractedClearsignedMessage; + } + + return textFromPart; + } + /** * Get the name of the message part. * @@ -504,7 +516,7 @@ public class MessageViewInfoExtractor { public final String text; public final String html; - public ViewableExtractedText(String text, String html) { + ViewableExtractedText(String text, String html) { this.text = text; this.html = html; } diff --git a/k9mail/src/main/java/com/fsck/k9/message/ComposePgpEnableByDefaultDecider.java b/k9mail/src/main/java/com/fsck/k9/message/ComposePgpEnableByDefaultDecider.java index 287e997ff..3350ce6e7 100644 --- a/k9mail/src/main/java/com/fsck/k9/message/ComposePgpEnableByDefaultDecider.java +++ b/k9mail/src/main/java/com/fsck/k9/message/ComposePgpEnableByDefaultDecider.java @@ -3,7 +3,7 @@ package com.fsck.k9.message; import java.util.List; -import com.fsck.k9.crypto.MessageDecryptVerifier; +import com.fsck.k9.crypto.MessageCryptoStructureDetector; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.Part; @@ -14,7 +14,7 @@ public class ComposePgpEnableByDefaultDecider { } private boolean messageIsEncrypted(Message localMessage) { - List encryptedParts = MessageDecryptVerifier.findEncryptedParts(localMessage); + List encryptedParts = MessageCryptoStructureDetector.findMultipartEncryptedParts(localMessage); return !encryptedParts.isEmpty(); } } diff --git a/k9mail/src/main/java/com/fsck/k9/message/ComposePgpInlineDecider.java b/k9mail/src/main/java/com/fsck/k9/message/ComposePgpInlineDecider.java index cef058c41..e3146b390 100644 --- a/k9mail/src/main/java/com/fsck/k9/message/ComposePgpInlineDecider.java +++ b/k9mail/src/main/java/com/fsck/k9/message/ComposePgpInlineDecider.java @@ -3,7 +3,7 @@ package com.fsck.k9.message; import java.util.List; -import com.fsck.k9.crypto.MessageDecryptVerifier; +import com.fsck.k9.crypto.MessageCryptoStructureDetector; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.Part; @@ -15,7 +15,7 @@ public class ComposePgpInlineDecider { } private boolean messageHasPgpInlineParts(Message localMessage) { - List inlineParts = MessageDecryptVerifier.findPgpInlineParts(localMessage); + List inlineParts = MessageCryptoStructureDetector.findPgpInlineParts(localMessage); return !inlineParts.isEmpty(); } } diff --git a/k9mail/src/main/java/com/fsck/k9/message/extractors/EncryptionDetector.java b/k9mail/src/main/java/com/fsck/k9/message/extractors/EncryptionDetector.java index f7a6f7f87..084c9c512 100644 --- a/k9mail/src/main/java/com/fsck/k9/message/extractors/EncryptionDetector.java +++ b/k9mail/src/main/java/com/fsck/k9/message/extractors/EncryptionDetector.java @@ -3,7 +3,7 @@ package com.fsck.k9.message.extractors; import android.support.annotation.NonNull; -import com.fsck.k9.crypto.MessageDecryptVerifier; +import com.fsck.k9.crypto.MessageCryptoStructureDetector; import com.fsck.k9.mail.Body; import com.fsck.k9.mail.BodyPart; import com.fsck.k9.mail.Message; @@ -31,7 +31,7 @@ class EncryptionDetector { private boolean containsInlinePgpEncryptedText(Message message) { Part textPart = textPartFinder.findFirstTextPart(message); - return MessageDecryptVerifier.isPartPgpInlineEncrypted(textPart); + return MessageCryptoStructureDetector.isPartPgpInlineEncrypted(textPart); } private boolean containsPartWithMimeType(Part part, String... wantedMimeTypes) { 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 751c76853..c4a181f03 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 @@ -19,7 +19,7 @@ import android.support.annotation.WorkerThread; import com.fsck.k9.K9; import com.fsck.k9.autocrypt.AutocryptOperations; -import com.fsck.k9.crypto.MessageDecryptVerifier; +import com.fsck.k9.crypto.MessageCryptoStructureDetector; import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Body; import com.fsck.k9.mail.BodyPart; @@ -52,7 +52,6 @@ import org.openintents.openpgp.util.OpenPgpApi.OpenPgpDataSink; 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; @@ -84,6 +83,7 @@ public class MessageCryptoHelper { private State state; private CancelableBackgroundOperation cancelableBackgroundOperation; private boolean isCancelled; + private boolean processSignedOnly; private OpenPgpApi openPgpApi; private OpenPgpServiceConnection openPgpServiceConnection; @@ -108,7 +108,7 @@ public class MessageCryptoHelper { } public void asyncStartOrResumeProcessingMessage(Message message, MessageCryptoCallback callback, - OpenPgpDecryptionResult cachedDecryptionResult) { + OpenPgpDecryptionResult cachedDecryptionResult, boolean processSignedOnly) { if (this.currentMessage != null) { reattachCallback(message, callback); return; @@ -119,21 +119,72 @@ public class MessageCryptoHelper { this.currentMessage = message; this.cachedDecryptionResult = cachedDecryptionResult; this.callback = callback; + this.processSignedOnly = processSignedOnly; nextStep(); } - private void findPartsForEncryptionPass() { - List encryptedParts = MessageDecryptVerifier.findEncryptedParts(currentMessage); - processFoundEncryptedParts(encryptedParts); + private void findPartsForMultipartEncryptionPass() { + List encryptedParts = MessageCryptoStructureDetector.findMultipartEncryptedParts(currentMessage); + for (Part part : encryptedParts) { + if (!MessageHelper.isCompletePartAvailable(part)) { + addErrorAnnotation(part, CryptoError.OPENPGP_ENCRYPTED_BUT_INCOMPLETE, MessageHelper.createEmptyPart()); + continue; + } + if (MessageCryptoStructureDetector.isMultipartEncryptedOpenPgpProtocol(part)) { + CryptoPart cryptoPart = new CryptoPart(CryptoPartType.PGP_ENCRYPTED, part); + partsToProcess.add(cryptoPart); + continue; + } + addErrorAnnotation(part, CryptoError.ENCRYPTED_BUT_UNSUPPORTED, MessageHelper.createEmptyPart()); + } } - private void findPartsForSignaturePass() { - List signedParts = MessageDecryptVerifier.findSignedParts(currentMessage, messageAnnotations); - processFoundSignedParts(signedParts); + private void findPartsForMultipartSignaturePass() { + List signedParts = MessageCryptoStructureDetector + .findMultipartSignedParts(currentMessage, messageAnnotations); + for (Part part : signedParts) { + if (!processSignedOnly) { + boolean isEncapsulatedSignature = + messageAnnotations.findKeyForAnnotationWithReplacementPart(part) != null; + if (!isEncapsulatedSignature) { + continue; + } + } + if (!MessageHelper.isCompletePartAvailable(part)) { + MimeBodyPart replacementPart = getMultipartSignedContentPartIfAvailable(part); + addErrorAnnotation(part, CryptoError.OPENPGP_SIGNED_BUT_INCOMPLETE, replacementPart); + continue; + } + if (MessageCryptoStructureDetector.isMultipartSignedOpenPgpProtocol(part)) { + CryptoPart cryptoPart = new CryptoPart(CryptoPartType.PGP_SIGNED, part); + partsToProcess.add(cryptoPart); + continue; + } + MimeBodyPart replacementPart = getMultipartSignedContentPartIfAvailable(part); + addErrorAnnotation(part, CryptoError.SIGNED_BUT_UNSUPPORTED, replacementPart); + } + } - List inlineParts = MessageDecryptVerifier.findPgpInlineParts(currentMessage); - processFoundInlinePgpParts(inlineParts); + private void findPartsForPgpInlinePass() { + List inlineParts = MessageCryptoStructureDetector.findPgpInlineParts(currentMessage); + for (Part part : inlineParts) { + if (!processSignedOnly && !MessageCryptoStructureDetector.isPartPgpInlineEncrypted(part)) { + continue; + } + + if (!currentMessage.getFlags().contains(Flag.X_DOWNLOADED_FULL)) { + if (MessageCryptoStructureDetector.isPartPgpInlineEncrypted(part)) { + addErrorAnnotation(part, CryptoError.OPENPGP_ENCRYPTED_BUT_INCOMPLETE, NO_REPLACEMENT_PART); + } else { + addErrorAnnotation(part, CryptoError.OPENPGP_SIGNED_BUT_INCOMPLETE, NO_REPLACEMENT_PART); + } + continue; + } + + CryptoPart cryptoPart = new CryptoPart(CryptoPartType.PGP_INLINE, part); + partsToProcess.add(cryptoPart); + } } private void findPartsForAutocryptPass() { @@ -148,60 +199,11 @@ public class MessageCryptoHelper { } } - private void processFoundEncryptedParts(List foundParts) { - for (Part part : foundParts) { - if (!MessageHelper.isCompletePartAvailable(part)) { - addErrorAnnotation(part, CryptoError.OPENPGP_ENCRYPTED_BUT_INCOMPLETE, MessageHelper.createEmptyPart()); - continue; - } - if (MessageDecryptVerifier.isPgpMimeEncryptedOrSignedPart(part)) { - CryptoPart cryptoPart = new CryptoPart(CryptoPartType.PGP_ENCRYPTED, part); - partsToProcess.add(cryptoPart); - continue; - } - addErrorAnnotation(part, CryptoError.ENCRYPTED_BUT_UNSUPPORTED, MessageHelper.createEmptyPart()); - } - } - - private void processFoundSignedParts(List foundParts) { - for (Part part : foundParts) { - if (!MessageHelper.isCompletePartAvailable(part)) { - MimeBodyPart replacementPart = getMultipartSignedContentPartIfAvailable(part); - addErrorAnnotation(part, CryptoError.OPENPGP_SIGNED_BUT_INCOMPLETE, replacementPart); - continue; - } - if (MessageDecryptVerifier.isPgpMimeEncryptedOrSignedPart(part)) { - CryptoPart cryptoPart = new CryptoPart(CryptoPartType.PGP_SIGNED, part); - partsToProcess.add(cryptoPart); - continue; - } - MimeBodyPart replacementPart = getMultipartSignedContentPartIfAvailable(part); - addErrorAnnotation(part, CryptoError.SIGNED_BUT_UNSUPPORTED, replacementPart); - } - } - private void addErrorAnnotation(Part part, CryptoError error, MimeBodyPart replacementPart) { CryptoResultAnnotation annotation = CryptoResultAnnotation.createErrorAnnotation(error, replacementPart); messageAnnotations.put(part, annotation); } - private void processFoundInlinePgpParts(List foundParts) { - for (Part part : foundParts) { - if (!currentMessage.getFlags().contains(Flag.X_DOWNLOADED_FULL)) { - if (MessageDecryptVerifier.isPartPgpInlineEncrypted(part)) { - addErrorAnnotation(part, CryptoError.OPENPGP_ENCRYPTED_BUT_INCOMPLETE, NO_REPLACEMENT_PART); - } else { - MimeBodyPart replacementPart = extractClearsignedTextReplacementPart(part); - addErrorAnnotation(part, CryptoError.OPENPGP_SIGNED_BUT_INCOMPLETE, replacementPart); - } - continue; - } - - CryptoPart cryptoPart = new CryptoPart(CryptoPartType.PGP_INLINE, part); - partsToProcess.add(cryptoPart); - } - } - private void nextStep() { if (isCancelled) { return; @@ -396,7 +398,7 @@ public class MessageCryptoHelper { private void callAsyncDetachedVerify(Intent intent) throws IOException, MessagingException { OpenPgpDataSource dataSource = getDataSourceForSignedData(currentCryptoPart.part); - byte[] signatureData = MessageDecryptVerifier.getSignatureData(currentCryptoPart.part); + byte[] signatureData = MessageCryptoStructureDetector.getSignatureData(currentCryptoPart.part); intent.putExtra(OpenPgpApi.EXTRA_DETACHED_SIGNATURE, signatureData); openPgpApi.executeApiAsync(intent, dataSource, new IOpenPgpSinkResultCallback() { @@ -645,17 +647,19 @@ public class MessageCryptoHelper { case START: { state = State.ENCRYPTION; - findPartsForEncryptionPass(); + findPartsForMultipartEncryptionPass(); return; } case ENCRYPTION: { - state = State.SIGNATURES; + state = State.SIGNATURES_AND_INLINE; - findPartsForSignaturePass(); + findPartsForMultipartSignaturePass(); + findPartsForPgpInlinePass(); return; } - case SIGNATURES: { + + case SIGNATURES_AND_INLINE: { state = State.AUTOCRYPT; findPartsForAutocryptPass(); @@ -780,22 +784,7 @@ public class MessageCryptoHelper { return replacementPart; } - private static MimeBodyPart extractClearsignedTextReplacementPart(Part part) { - try { - String clearsignedText = MessageExtractor.getTextFromPart(part); - String replacementText = OpenPgpUtils.extractClearsignedMessage(clearsignedText); - if (replacementText == null) { - Timber.e("failed to extract clearsigned text for replacement part"); - return NO_REPLACEMENT_PART; - } - return new MimeBodyPart(new TextBody(replacementText), "text/plain"); - } catch (MessagingException e) { - Timber.e(e, "failed to create clearsigned text replacement part"); - return NO_REPLACEMENT_PART; - } - } - private enum State { - START, ENCRYPTION, SIGNATURES, AUTOCRYPT, FINISHED + START, ENCRYPTION, SIGNATURES_AND_INLINE, AUTOCRYPT, FINISHED } } diff --git a/k9mail/src/main/java/com/fsck/k9/ui/crypto/MessageCryptoSplitter.java b/k9mail/src/main/java/com/fsck/k9/ui/crypto/MessageCryptoSplitter.java index e0ef238fb..02c439eff 100644 --- a/k9mail/src/main/java/com/fsck/k9/ui/crypto/MessageCryptoSplitter.java +++ b/k9mail/src/main/java/com/fsck/k9/ui/crypto/MessageCryptoSplitter.java @@ -8,7 +8,7 @@ import java.util.List; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import com.fsck.k9.crypto.MessageDecryptVerifier; +import com.fsck.k9.crypto.MessageCryptoStructureDetector; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.Part; import com.fsck.k9.mailstore.CryptoResultAnnotation; @@ -21,7 +21,7 @@ public class MessageCryptoSplitter { @Nullable public static CryptoMessageParts split(@NonNull Message message, @Nullable MessageCryptoAnnotations annotations) { ArrayList extraParts = new ArrayList<>(); - Part primaryPart = MessageDecryptVerifier.findPrimaryEncryptedOrSignedPart(message, extraParts); + Part primaryPart = MessageCryptoStructureDetector.findPrimaryEncryptedOrSignedPart(message, extraParts); if (primaryPart == null) { return null; } diff --git a/k9mail/src/test/java/com/fsck/k9/crypto/MessageDecryptVerifierTest.java b/k9mail/src/test/java/com/fsck/k9/crypto/MessageCryptoStructureDetectorTest.java similarity index 58% rename from k9mail/src/test/java/com/fsck/k9/crypto/MessageDecryptVerifierTest.java rename to k9mail/src/test/java/com/fsck/k9/crypto/MessageCryptoStructureDetectorTest.java index ff5fa20ce..92c968c9f 100644 --- a/k9mail/src/test/java/com/fsck/k9/crypto/MessageDecryptVerifierTest.java +++ b/k9mail/src/test/java/com/fsck/k9/crypto/MessageCryptoStructureDetectorTest.java @@ -7,20 +7,19 @@ import java.util.List; import com.fsck.k9.K9RobolectricTestRunner; import com.fsck.k9.mail.BodyPart; import com.fsck.k9.mail.Message; -import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Multipart; import com.fsck.k9.mail.Part; -import com.fsck.k9.mail.internet.MimeBodyPart; -import com.fsck.k9.mail.internet.MimeHeader; import com.fsck.k9.mail.internet.MimeMessage; import com.fsck.k9.mail.internet.MimeMessageHelper; -import com.fsck.k9.mail.internet.MimeMultipart; import com.fsck.k9.mail.internet.TextBody; import com.fsck.k9.ui.crypto.MessageCryptoAnnotations; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.annotation.Config; +import static com.fsck.k9.message.TestMessageConstructionUtils.bodypart; +import static com.fsck.k9.message.TestMessageConstructionUtils.messageFromBody; +import static com.fsck.k9.message.TestMessageConstructionUtils.multipart; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; @@ -29,13 +28,12 @@ import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; +@SuppressWarnings("WeakerAccess") @RunWith(K9RobolectricTestRunner.class) @Config(manifest = Config.NONE) -public class MessageDecryptVerifierTest { - private static final String MIME_TYPE_MULTIPART_ENCRYPTED = "multipart/encrypted"; - private MessageCryptoAnnotations messageCryptoAnnotations = mock(MessageCryptoAnnotations.class); - private static final String PROTCOL_PGP_ENCRYPTED = "application/pgp-encrypted"; - private static final String PGP_INLINE_DATA = "" + +public class MessageCryptoStructureDetectorTest { + MessageCryptoAnnotations messageCryptoAnnotations = mock(MessageCryptoAnnotations.class); + static final String PGP_INLINE_DATA = "" + "-----BEGIN PGP MESSAGE-----\n" + "Header: Value\n" + "\n" + @@ -49,7 +47,7 @@ public class MessageDecryptVerifierTest { Message message = new MimeMessage(); MimeMessageHelper.setBody(message, new TextBody(PGP_INLINE_DATA)); - Part cryptoPart = MessageDecryptVerifier.findPrimaryEncryptedOrSignedPart(message, outputExtraParts); + Part cryptoPart = MessageCryptoStructureDetector.findPrimaryEncryptedOrSignedPart(message, outputExtraParts); assertSame(message, cryptoPart); } @@ -60,12 +58,12 @@ public class MessageDecryptVerifierTest { BodyPart pgpInlinePart = bodypart("text/plain", PGP_INLINE_DATA); Message message = messageFromBody( multipart("alternative", - pgpInlinePart, + pgpInlinePart, bodypart("text/html") ) ); - Part cryptoPart = MessageDecryptVerifier.findPrimaryEncryptedOrSignedPart(message, outputExtraParts); + Part cryptoPart = MessageCryptoStructureDetector.findPrimaryEncryptedOrSignedPart(message, outputExtraParts); assertSame(pgpInlinePart, cryptoPart); } @@ -81,7 +79,7 @@ public class MessageDecryptVerifierTest { ) ); - Part cryptoPart = MessageDecryptVerifier.findPrimaryEncryptedOrSignedPart(message, outputExtraParts); + Part cryptoPart = MessageCryptoStructureDetector.findPrimaryEncryptedOrSignedPart(message, outputExtraParts); assertSame(pgpInlinePart, cryptoPart); } @@ -101,7 +99,7 @@ public class MessageDecryptVerifierTest { ) ); - Part cryptoPart = MessageDecryptVerifier.findPrimaryEncryptedOrSignedPart(message, outputExtraParts); + Part cryptoPart = MessageCryptoStructureDetector.findPrimaryEncryptedOrSignedPart(message, outputExtraParts); assertSame(pgpInlinePart, cryptoPart); } @@ -113,7 +111,7 @@ public class MessageDecryptVerifierTest { multipart("alternative") ); - Part cryptoPart = MessageDecryptVerifier.findPrimaryEncryptedOrSignedPart(message, outputExtraParts); + Part cryptoPart = MessageCryptoStructureDetector.findPrimaryEncryptedOrSignedPart(message, outputExtraParts); assertNull(cryptoPart); } @@ -125,7 +123,7 @@ public class MessageDecryptVerifierTest { multipart("mixed") ); - Part cryptoPart = MessageDecryptVerifier.findPrimaryEncryptedOrSignedPart(message, outputExtraParts); + Part cryptoPart = MessageCryptoStructureDetector.findPrimaryEncryptedOrSignedPart(message, outputExtraParts); assertNull(cryptoPart); } @@ -140,7 +138,7 @@ public class MessageDecryptVerifierTest { ) ); - Part cryptoPart = MessageDecryptVerifier.findPrimaryEncryptedOrSignedPart(message, outputExtraParts); + Part cryptoPart = MessageCryptoStructureDetector.findPrimaryEncryptedOrSignedPart(message, outputExtraParts); assertNull(cryptoPart); } @@ -149,7 +147,7 @@ public class MessageDecryptVerifierTest { public void findEncryptedPartsShouldReturnEmptyListForEmptyMessage() throws Exception { MimeMessage emptyMessage = new MimeMessage(); - List encryptedParts = MessageDecryptVerifier.findEncryptedParts(emptyMessage); + List encryptedParts = MessageCryptoStructureDetector.findMultipartEncryptedParts(emptyMessage); assertEquals(0, encryptedParts.size()); } @@ -159,56 +157,42 @@ public class MessageDecryptVerifierTest { MimeMessage message = new MimeMessage(); message.setBody(new TextBody("message text")); - List encryptedParts = MessageDecryptVerifier.findEncryptedParts(message); + List encryptedParts = MessageCryptoStructureDetector.findMultipartEncryptedParts(message); assertEquals(0, encryptedParts.size()); } @Test - public void findEncryptedPartsShouldReturnEmptyEncryptedPart() throws Exception { - MimeMessage message = new MimeMessage(); - MimeMultipart multipartEncrypted = MimeMultipart.newInstance(); - multipartEncrypted.setSubType("encrypted"); - MimeMessageHelper.setBody(message, multipartEncrypted); - setContentTypeWithProtocol(message, MIME_TYPE_MULTIPART_ENCRYPTED, PROTCOL_PGP_ENCRYPTED); + public void findEncrypted__withMultipartEncrypted__shouldReturnRoot() throws Exception { + Message message = messageFromBody( + multipart("encrypted", "protocol=\"application/pgp-encrypted\"", + bodypart("application/pgp-encrypted"), + bodypart("application/octet-stream") + ) + ); - List encryptedParts = MessageDecryptVerifier.findEncryptedParts(message); + List encryptedParts = MessageCryptoStructureDetector.findMultipartEncryptedParts(message); assertEquals(1, encryptedParts.size()); assertSame(message, encryptedParts.get(0)); } @Test - public void findEncryptedPartsShouldReturnMultipleEncryptedParts() throws Exception { - MimeMessage message = new MimeMessage(); - MimeMultipart multipartMixed = MimeMultipart.newInstance(); - multipartMixed.setSubType("mixed"); - MimeMessageHelper.setBody(message, multipartMixed); + public void findEncrypted__withBadProtocol__shouldReturnEmpty() throws Exception { + Message message = messageFromBody( + multipart("encrypted", "protocol=\"application/not-pgp-encrypted\"", + bodypart("application/pgp-encrypted"), + bodypart("application/octet-stream", "content") + ) + ); - MimeMultipart multipartEncryptedOne = MimeMultipart.newInstance(); - multipartEncryptedOne.setSubType("encrypted"); - MimeBodyPart bodyPartOne = new MimeBodyPart(multipartEncryptedOne); - setContentTypeWithProtocol(bodyPartOne, MIME_TYPE_MULTIPART_ENCRYPTED, PROTCOL_PGP_ENCRYPTED); - multipartMixed.addBodyPart(bodyPartOne); + List encryptedParts = MessageCryptoStructureDetector.findMultipartEncryptedParts(message); - MimeBodyPart bodyPartTwo = new MimeBodyPart(null, "text/plain"); - multipartMixed.addBodyPart(bodyPartTwo); - - MimeMultipart multipartEncryptedThree = MimeMultipart.newInstance(); - multipartEncryptedThree.setSubType("encrypted"); - MimeBodyPart bodyPartThree = new MimeBodyPart(multipartEncryptedThree); - setContentTypeWithProtocol(bodyPartThree, MIME_TYPE_MULTIPART_ENCRYPTED, PROTCOL_PGP_ENCRYPTED); - multipartMixed.addBodyPart(bodyPartThree); - - List encryptedParts = MessageDecryptVerifier.findEncryptedParts(message); - - assertEquals(2, encryptedParts.size()); - assertSame(bodyPartOne, encryptedParts.get(0)); - assertSame(bodyPartThree, encryptedParts.get(1)); + assertTrue(encryptedParts.isEmpty()); } @Test - public void findEncrypted__withMultipartEncrypted__shouldReturnRoot() throws Exception { + public void findEncrypted__withBadProtocolAndNoBody__shouldReturnRoot() throws Exception { Message message = messageFromBody( multipart("encrypted", bodypart("application/pgp-encrypted"), @@ -216,24 +200,64 @@ public class MessageDecryptVerifierTest { ) ); - List encryptedParts = MessageDecryptVerifier.findEncryptedParts(message); + List encryptedParts = MessageCryptoStructureDetector.findMultipartEncryptedParts(message); assertEquals(1, encryptedParts.size()); assertSame(message, encryptedParts.get(0)); } + @Test + public void findEncrypted__withEmptyProtocol__shouldReturnEmpty() throws Exception { + Message message = messageFromBody( + multipart("encrypted", + bodypart("application/pgp-encrypted"), + bodypart("application/octet-stream", "content") + ) + ); + + List encryptedParts = MessageCryptoStructureDetector.findMultipartEncryptedParts(message); + + assertTrue(encryptedParts.isEmpty()); + } + + @Test + public void findEncrypted__withMissingEncryptedBody__shouldReturnEmpty() throws Exception { + Message message = messageFromBody( + multipart("encrypted", "protocol=\"application/pgp-encrypted\"", + bodypart("application/pgp-encrypted") + ) + ); + + List encryptedParts = MessageCryptoStructureDetector.findMultipartEncryptedParts(message); + + assertTrue(encryptedParts.isEmpty()); + } + + @Test + public void findEncrypted__withBadStructure__shouldReturnEmpty() throws Exception { + Message message = messageFromBody( + multipart("encrypted", "protocol=\"application/pgp-encrypted\"", + bodypart("application/octet-stream") + ) + ); + + List encryptedParts = MessageCryptoStructureDetector.findMultipartEncryptedParts(message); + + assertTrue(encryptedParts.isEmpty()); + } + @Test public void findEncrypted__withMultipartMixedSubEncrypted__shouldReturnRoot() throws Exception { Message message = messageFromBody( multipart("mixed", - multipart("encrypted", + multipart("encrypted", "protocol=\"application/pgp-encrypted\"", bodypart("application/pgp-encrypted"), bodypart("application/octet-stream") ) ) ); - List encryptedParts = MessageDecryptVerifier.findEncryptedParts(message); + List encryptedParts = MessageCryptoStructureDetector.findMultipartEncryptedParts(message); assertEquals(1, encryptedParts.size()); assertSame(getPart(message, 0), encryptedParts.get(0)); @@ -244,18 +268,18 @@ public class MessageDecryptVerifierTest { throws Exception { Message message = messageFromBody( multipart("mixed", - multipart("encrypted", + multipart("encrypted", "protocol=\"application/pgp-encrypted\"", bodypart("application/pgp-encrypted"), bodypart("application/octet-stream") ), - multipart("encrypted", + multipart("encrypted", "protocol=\"application/pgp-encrypted\"", bodypart("application/pgp-encrypted"), bodypart("application/octet-stream") ) ) ); - List encryptedParts = MessageDecryptVerifier.findEncryptedParts(message); + List encryptedParts = MessageCryptoStructureDetector.findMultipartEncryptedParts(message); assertEquals(2, encryptedParts.size()); assertSame(getPart(message, 0), encryptedParts.get(0)); @@ -267,14 +291,14 @@ public class MessageDecryptVerifierTest { Message message = messageFromBody( multipart("mixed", bodypart("text/plain"), - multipart("encrypted", + multipart("encrypted", "protocol=\"application/pgp-encrypted\"", bodypart("application/pgp-encrypted"), bodypart("application/octet-stream") ) ) ); - List encryptedParts = MessageDecryptVerifier.findEncryptedParts(message); + List encryptedParts = MessageCryptoStructureDetector.findMultipartEncryptedParts(message); assertEquals(1, encryptedParts.size()); assertSame(getPart(message, 1), encryptedParts.get(0)); @@ -284,7 +308,7 @@ public class MessageDecryptVerifierTest { public void findEncrypted__withMultipartMixedSubEncryptedAndText__shouldReturnEncrypted() throws Exception { Message message = messageFromBody( multipart("mixed", - multipart("encrypted", + multipart("encrypted", "protocol=\"application/pgp-encrypted\"", bodypart("application/pgp-encrypted"), bodypart("application/octet-stream") ), @@ -292,7 +316,7 @@ public class MessageDecryptVerifierTest { ) ); - List encryptedParts = MessageDecryptVerifier.findEncryptedParts(message); + List encryptedParts = MessageCryptoStructureDetector.findMultipartEncryptedParts(message); assertEquals(1, encryptedParts.size()); assertSame(getPart(message, 0), encryptedParts.get(0)); @@ -301,22 +325,83 @@ public class MessageDecryptVerifierTest { @Test public void findSigned__withSimpleMultipartSigned__shouldReturnRoot() throws Exception { Message message = messageFromBody( - multipart("signed", + multipart("signed", "protocol=\"application/pgp-signature\"", bodypart("text/plain"), bodypart("application/pgp-signature") ) ); - List signedParts = MessageDecryptVerifier.findSignedParts(message, messageCryptoAnnotations); + List signedParts = MessageCryptoStructureDetector + .findMultipartSignedParts(message, messageCryptoAnnotations); assertEquals(1, signedParts.size()); assertSame(message, signedParts.get(0)); } @Test - public void findSigned__withComplexMultipartSigned__shouldReturnRoot() throws Exception { + public void findSigned__withNoProtocolAndNoBody__shouldReturnRoot() throws Exception { Message message = messageFromBody( multipart("signed", + bodypart("text/plain"), + bodypart("application/pgp-signature") + ) + ); + + List signedParts = MessageCryptoStructureDetector + .findMultipartSignedParts(message, messageCryptoAnnotations); + + assertEquals(1, signedParts.size()); + assertSame(message, signedParts.get(0)); + } + + @Test + public void findSigned__withBadProtocol__shouldReturnNothing() throws Exception { + Message message = messageFromBody( + multipart("signed", "protocol=\"application/not-pgp-signature\"", + bodypart("text/plain", "content"), + bodypart("application/pgp-signature") + ) + ); + + List signedParts = MessageCryptoStructureDetector + .findMultipartSignedParts(message, messageCryptoAnnotations); + + assertTrue(signedParts.isEmpty()); + } + + @Test + public void findSigned__withEmptyProtocol__shouldReturnRoot() throws Exception { + Message message = messageFromBody( + multipart("signed", + bodypart("text/plain", "content"), + bodypart("application/pgp-signature") + ) + ); + + List signedParts = MessageCryptoStructureDetector + .findMultipartSignedParts(message, messageCryptoAnnotations); + + assertTrue(signedParts.isEmpty()); + } + + @Test + public void findSigned__withMissingSignature__shouldReturnEmpty() throws Exception { + Message message = messageFromBody( + multipart("signed", "protocol=\"application/pgp-signature\"", + bodypart("text/plain") + ) + ); + + List signedParts = MessageCryptoStructureDetector + .findMultipartSignedParts(message, messageCryptoAnnotations); + + assertTrue(signedParts.isEmpty()); + } + + @Test + public void findSigned__withComplexMultipartSigned__shouldReturnRoot() throws Exception { + Message message = messageFromBody( + multipart("signed", "protocol=\"application/pgp-signature\"", multipart("mixed", bodypart("text/plain"), bodypart("application/pdf") @@ -325,7 +410,8 @@ public class MessageDecryptVerifierTest { ) ); - List signedParts = MessageDecryptVerifier.findSignedParts(message, messageCryptoAnnotations); + List signedParts = MessageCryptoStructureDetector + .findMultipartSignedParts(message, messageCryptoAnnotations); assertEquals(1, signedParts.size()); assertSame(message, signedParts.get(0)); @@ -335,14 +421,15 @@ public class MessageDecryptVerifierTest { public void findEncrypted__withMultipartMixedSubSigned__shouldReturnSigned() throws Exception { Message message = messageFromBody( multipart("mixed", - multipart("signed", - bodypart("text/plain"), - bodypart("application/pgp-signature") + multipart("signed", "protocol=\"application/pgp-signature\"", + bodypart("text/plain"), + bodypart("application/pgp-signature") ) ) ); - List signedParts = MessageDecryptVerifier.findSignedParts(message, messageCryptoAnnotations); + List signedParts = MessageCryptoStructureDetector + .findMultipartSignedParts(message, messageCryptoAnnotations); assertEquals(1, signedParts.size()); assertSame(getPart(message, 0), signedParts.get(0)); @@ -352,7 +439,7 @@ public class MessageDecryptVerifierTest { public void findEncrypted__withMultipartMixedSubSignedAndText__shouldReturnSigned() throws Exception { Message message = messageFromBody( multipart("mixed", - multipart("signed", + multipart("signed", "application/pgp-signature", bodypart("text/plain"), bodypart("application/pgp-signature") ), @@ -360,7 +447,8 @@ public class MessageDecryptVerifierTest { ) ); - List signedParts = MessageDecryptVerifier.findSignedParts(message, messageCryptoAnnotations); + List signedParts = MessageCryptoStructureDetector + .findMultipartSignedParts(message, messageCryptoAnnotations); assertEquals(1, signedParts.size()); assertSame(getPart(message, 0), signedParts.get(0)); @@ -371,14 +459,15 @@ public class MessageDecryptVerifierTest { Message message = messageFromBody( multipart("mixed", bodypart("text/plain"), - multipart("signed", + multipart("signed", "application/pgp-signature", bodypart("text/plain"), bodypart("application/pgp-signature") ) ) ); - List signedParts = MessageDecryptVerifier.findSignedParts(message, messageCryptoAnnotations); + List signedParts = MessageCryptoStructureDetector + .findMultipartSignedParts(message, messageCryptoAnnotations); assertEquals(1, signedParts.size()); assertSame(getPart(message, 1), signedParts.get(0)); @@ -395,7 +484,7 @@ public class MessageDecryptVerifierTest { MimeMessage message = new MimeMessage(); message.setBody(new TextBody(pgpInlineData)); - assertTrue(MessageDecryptVerifier.isPartPgpInlineEncrypted(message)); + assertTrue(MessageCryptoStructureDetector.isPartPgpInlineEncrypted(message)); } @Test @@ -410,8 +499,8 @@ public class MessageDecryptVerifierTest { MimeMessage message = new MimeMessage(); message.setBody(new TextBody(pgpInlineData)); - assertTrue(MessageDecryptVerifier.isPartPgpInlineEncryptedOrSigned(message)); - assertTrue(MessageDecryptVerifier.isPartPgpInlineEncrypted(message)); + assertTrue(MessageCryptoStructureDetector.isPartPgpInlineEncryptedOrSigned(message)); + assertTrue(MessageCryptoStructureDetector.isPartPgpInlineEncrypted(message)); } @Test @@ -426,8 +515,8 @@ public class MessageDecryptVerifierTest { MimeMessage message = new MimeMessage(); message.setBody(new TextBody(pgpInlineData)); - assertFalse(MessageDecryptVerifier.isPartPgpInlineEncryptedOrSigned(message)); - assertFalse(MessageDecryptVerifier.isPartPgpInlineEncrypted(message)); + assertFalse(MessageCryptoStructureDetector.isPartPgpInlineEncryptedOrSigned(message)); + assertFalse(MessageCryptoStructureDetector.isPartPgpInlineEncrypted(message)); } @Test @@ -444,7 +533,7 @@ public class MessageDecryptVerifierTest { MimeMessage message = new MimeMessage(); message.setBody(new TextBody(pgpInlineData)); - assertTrue(MessageDecryptVerifier.isPartPgpInlineEncryptedOrSigned(message)); + assertTrue(MessageCryptoStructureDetector.isPartPgpInlineEncryptedOrSigned(message)); } @Test @@ -461,44 +550,14 @@ public class MessageDecryptVerifierTest { MimeMessage message = new MimeMessage(); message.setBody(new TextBody(pgpInlineData)); - assertFalse(MessageDecryptVerifier.isPartPgpInlineEncrypted(message)); + assertFalse(MessageCryptoStructureDetector.isPartPgpInlineEncrypted(message)); } - MimeMessage messageFromBody(BodyPart bodyPart) throws MessagingException { - MimeMessage message = new MimeMessage(); - MimeMessageHelper.setBody(message, bodyPart.getBody()); - return message; - } - - MimeBodyPart multipart(String type, BodyPart... subParts) throws MessagingException { - MimeMultipart multiPart = MimeMultipart.newInstance(); - multiPart.setSubType(type); - for (BodyPart subPart : subParts) { - multiPart.addBodyPart(subPart); - } - return new MimeBodyPart(multiPart); - } - - BodyPart bodypart(String type) throws MessagingException { - return new MimeBodyPart(null, type); - } - - BodyPart bodypart(String type, String text) throws MessagingException { - TextBody textBody = new TextBody(text); - return new MimeBodyPart(textBody, type); - } - - public static Part getPart(Part searchRootPart, int... indexes) { + static Part getPart(Part searchRootPart, int... indexes) { Part part = searchRootPart; for (int index : indexes) { part = ((Multipart) part.getBody()).getBodyPart(index); } return part; } - - //TODO: Find a cleaner way to do this - private static void setContentTypeWithProtocol(Part part, String mimeType, String protocol) - throws MessagingException { - part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, mimeType + "; protocol=\"" + protocol + "\""); - } } diff --git a/k9mail/src/test/java/com/fsck/k9/message/TestMessageConstructionUtils.java b/k9mail/src/test/java/com/fsck/k9/message/TestMessageConstructionUtils.java new file mode 100644 index 000000000..45ee4b908 --- /dev/null +++ b/k9mail/src/test/java/com/fsck/k9/message/TestMessageConstructionUtils.java @@ -0,0 +1,56 @@ +package com.fsck.k9.message; + + +import com.fsck.k9.mail.Body; +import com.fsck.k9.mail.BodyPart; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.internet.MimeBodyPart; +import com.fsck.k9.mail.internet.MimeHeader; +import com.fsck.k9.mail.internet.MimeMessage; +import com.fsck.k9.mail.internet.MimeMessageHelper; +import com.fsck.k9.mail.internet.MimeMultipart; +import com.fsck.k9.mail.internet.TextBody; + + +public class TestMessageConstructionUtils { + public static MimeMessage messageFromBody(BodyPart bodyPart) throws MessagingException { + MimeMessage message = new MimeMessage(); + MimeMessageHelper.setBody(message, bodyPart.getBody()); + if (bodyPart.getContentType() != null) { + message.setHeader("Content-Type", bodyPart.getContentType()); + } + message.setUid("msguid"); + return message; + } + + public static MimeBodyPart multipart(String type, BodyPart... subParts) throws MessagingException { + return multipart(type, null, subParts); + } + + public static MimeBodyPart multipart(String type, String typeParameters, BodyPart... subParts) throws MessagingException { + MimeMultipart multiPart = MimeMultipart.newInstance(); + multiPart.setSubType(type); + for (BodyPart subPart : subParts) { + multiPart.addBodyPart(subPart); + } + MimeBodyPart mimeBodyPart = new MimeBodyPart(multiPart); + if (typeParameters != null) { + mimeBodyPart.setHeader(MimeHeader.HEADER_CONTENT_TYPE, + mimeBodyPart.getContentType() + "; " + typeParameters); + } + return mimeBodyPart; + } + + public static BodyPart bodypart(String type) throws MessagingException { + return new MimeBodyPart(null, type); + } + + public static BodyPart bodypart(String type, String text) throws MessagingException { + TextBody textBody = new TextBody(text); + return new MimeBodyPart(textBody, type); + } + + public static BodyPart bodypart(String type, Body body) throws MessagingException { + return new MimeBodyPart(body, type); + } +} 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 index 6f102d79c..dd97caabf 100644 --- a/k9mail/src/test/java/com/fsck/k9/ui/crypto/MessageCryptoHelperTest.java +++ b/k9mail/src/test/java/com/fsck/k9/ui/crypto/MessageCryptoHelperTest.java @@ -14,10 +14,8 @@ 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; @@ -37,11 +35,13 @@ import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; +import static com.fsck.k9.message.TestMessageConstructionUtils.bodypart; +import static com.fsck.k9.message.TestMessageConstructionUtils.messageFromBody; +import static com.fsck.k9.message.TestMessageConstructionUtils.multipart; 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.Matchers.eq; import static org.mockito.Matchers.same; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; @@ -84,7 +84,7 @@ public class MessageCryptoHelperTest { message.setHeader("Content-Type", "text/plain"); MessageCryptoCallback messageCryptoCallback = mock(MessageCryptoCallback.class); - messageCryptoHelper.asyncStartOrResumeProcessingMessage(message, messageCryptoCallback, null); + messageCryptoHelper.asyncStartOrResumeProcessingMessage(message, messageCryptoCallback, null, false); ArgumentCaptor captor = ArgumentCaptor.forClass(MessageCryptoAnnotations.class); verify(messageCryptoCallback).onCryptoOperationsFinished(captor.capture()); @@ -107,7 +107,7 @@ public class MessageCryptoHelperTest { MessageCryptoCallback messageCryptoCallback = mock(MessageCryptoCallback.class); - messageCryptoHelper.asyncStartOrResumeProcessingMessage(message, messageCryptoCallback, null); + messageCryptoHelper.asyncStartOrResumeProcessingMessage(message, messageCryptoCallback, null, false); ArgumentCaptor captor = ArgumentCaptor.forClass(MessageCryptoAnnotations.class); @@ -124,12 +124,15 @@ public class MessageCryptoHelperTest { @Test public void multipartSigned__withNullBody__shouldReturnSignedIncomplete() throws Exception { - MimeMessage message = new MimeMessage(); - message.setUid("msguid"); - message.setHeader("Content-Type", "multipart/signed"); + Message message = messageFromBody( + multipart("signed", "protocol=\"application/pgp-signature\"", + bodypart("text/plain"), + bodypart("application/pgp-signature") + ) + ); MessageCryptoCallback messageCryptoCallback = mock(MessageCryptoCallback.class); - messageCryptoHelper.asyncStartOrResumeProcessingMessage(message, messageCryptoCallback, null); + messageCryptoHelper.asyncStartOrResumeProcessingMessage(message, messageCryptoCallback, null, true); assertPartAnnotationHasState(message, messageCryptoCallback, CryptoError.OPENPGP_SIGNED_BUT_INCOMPLETE, null, null, null, null); @@ -137,12 +140,15 @@ public class MessageCryptoHelperTest { @Test public void multipartEncrypted__withNullBody__shouldReturnEncryptedIncomplete() throws Exception { - MimeMessage message = new MimeMessage(); - message.setUid("msguid"); - message.setHeader("Content-Type", "multipart/encrypted"); + Message message = messageFromBody( + multipart("encrypted", "protocol=\"application/pgp-encrypted\"", + bodypart("application/pgp-encrypted"), + bodypart("application/octet-stream") + ) + ); MessageCryptoCallback messageCryptoCallback = mock(MessageCryptoCallback.class); - messageCryptoHelper.asyncStartOrResumeProcessingMessage(message, messageCryptoCallback, null); + messageCryptoHelper.asyncStartOrResumeProcessingMessage(message, messageCryptoCallback, null, false); assertPartAnnotationHasState( message, messageCryptoCallback, CryptoError.OPENPGP_ENCRYPTED_BUT_INCOMPLETE, null, null, null, null); @@ -150,13 +156,15 @@ public class MessageCryptoHelperTest { @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", "--------")); + Message message = messageFromBody( + multipart("encrypted", "protocol=\"application/bad-protocol\"", + bodypart("application/bad-protocol", "content"), + bodypart("application/octet-stream", "content") + ) + ); MessageCryptoCallback messageCryptoCallback = mock(MessageCryptoCallback.class); - messageCryptoHelper.asyncStartOrResumeProcessingMessage(message, messageCryptoCallback, null); + messageCryptoHelper.asyncStartOrResumeProcessingMessage(message, messageCryptoCallback, null, false); assertPartAnnotationHasState(message, messageCryptoCallback, CryptoError.ENCRYPTED_BUT_UNSUPPORTED, null, null, null, null); @@ -164,13 +172,15 @@ public class MessageCryptoHelperTest { @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", "--------")); + Message message = messageFromBody( + multipart("signed", "protocol=\"application/bad-protocol\"", + bodypart("text/plain", "content"), + bodypart("application/bad-protocol", "content") + ) + ); MessageCryptoCallback messageCryptoCallback = mock(MessageCryptoCallback.class); - messageCryptoHelper.asyncStartOrResumeProcessingMessage(message, messageCryptoCallback, null); + messageCryptoHelper.asyncStartOrResumeProcessingMessage(message, messageCryptoCallback, null, true); assertPartAnnotationHasState(message, messageCryptoCallback, CryptoError.SIGNED_BUT_UNSUPPORTED, null, null, null, null); @@ -178,18 +188,14 @@ public class MessageCryptoHelperTest { @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\""); + BodyPart signedBodyPart = spy(bodypart("text/plain", "content")); + Message message = messageFromBody( + multipart("signed", "protocol=\"application/pgp-signature\"", + signedBodyPart, + bodypart("application/pgp-signature", "content") + ) + ); message.setFrom(Address.parse("Test ")[0]); - message.setBody(messageBody); OutputStream outputStream = mock(OutputStream.class); @@ -204,21 +210,46 @@ public class MessageCryptoHelperTest { verifyNoMoreInteractions(autocryptOperations); } + @Test + public void multipartSigned__withSignOnlyDisabled__shouldReturnNothing() throws Exception { + Message message = messageFromBody( + multipart("signed", "protocol=\"application/pgp-signature\"", + bodypart("text/plain", "content"), + bodypart("application/pgp-signature", "content") + ) + ); + + MessageCryptoCallback messageCryptoCallback = mock(MessageCryptoCallback.class); + messageCryptoHelper.asyncStartOrResumeProcessingMessage(message, messageCryptoCallback, null, false); + + assertReturnsWithNoCryptoAnnotations(messageCryptoCallback); + } + + @Test + public void multipartSigned__withSignOnlyDisabledAndNullBody__shouldReturnNothing() throws Exception { + Message message = messageFromBody( + multipart("signed", "protocol=\"application/pgp-signature\"", + bodypart("text/plain"), + bodypart("application/pgp-signature") + ) + ); + + MessageCryptoCallback messageCryptoCallback = mock(MessageCryptoCallback.class); + messageCryptoHelper.asyncStartOrResumeProcessingMessage(message, messageCryptoCallback, null, false); + + assertReturnsWithNoCryptoAnnotations(messageCryptoCallback); + } + @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 message = messageFromBody( + multipart("encrypted", "protocol=\"application/pgp-encrypted\"", + bodypart("application/pgp-encrypted", "content"), + bodypart("application/octet-stream", encryptedBody) + ) + ); message.setFrom(Address.parse("Test ")[0]); - message.setBody(messageBody); OutputStream outputStream = mock(OutputStream.class); @@ -248,7 +279,7 @@ public class MessageCryptoHelperTest { private void processEncryptedMessageAndCaptureMocks(Message message, Body encryptedBody, OutputStream outputStream) throws Exception { - messageCryptoHelper.asyncStartOrResumeProcessingMessage(message, messageCryptoCallback, null); + messageCryptoHelper.asyncStartOrResumeProcessingMessage(message, messageCryptoCallback, null, false); ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); ArgumentCaptor dataSourceCaptor = ArgumentCaptor.forClass(OpenPgpDataSource.class); @@ -266,7 +297,7 @@ public class MessageCryptoHelperTest { private void processSignedMessageAndCaptureMocks(Message message, BodyPart signedBodyPart, OutputStream outputStream) throws Exception { - messageCryptoHelper.asyncStartOrResumeProcessingMessage(message, messageCryptoCallback, null); + messageCryptoHelper.asyncStartOrResumeProcessingMessage(message, messageCryptoCallback, null, true); ArgumentCaptor intentCaptor = ArgumentCaptor.forClass(Intent.class); ArgumentCaptor dataSourceCaptor = ArgumentCaptor.forClass(OpenPgpDataSource.class); @@ -282,6 +313,15 @@ public class MessageCryptoHelperTest { verify(signedBodyPart).writeTo(outputStream); } + private void assertReturnsWithNoCryptoAnnotations(MessageCryptoCallback messageCryptoCallback) { + ArgumentCaptor captor = ArgumentCaptor.forClass(MessageCryptoAnnotations.class); + verify(messageCryptoCallback).onCryptoOperationsFinished(captor.capture()); + verifyNoMoreInteractions(messageCryptoCallback); + + MessageCryptoAnnotations annotations = captor.getValue(); + assertTrue(annotations.isEmpty()); + } + private void assertPartAnnotationHasState(Message message, MessageCryptoCallback messageCryptoCallback, CryptoError cryptoErrorState, MimeBodyPart replacementPart, OpenPgpDecryptionResult openPgpDecryptionResult, OpenPgpSignatureResult openPgpSignatureResult, PendingIntent openPgpPendingIntent) { diff --git a/plugins/openpgp-api-lib/openpgp-api/src/main/java/org/openintents/openpgp/util/OpenPgpUtils.java b/plugins/openpgp-api-lib/openpgp-api/src/main/java/org/openintents/openpgp/util/OpenPgpUtils.java index 4840a2119..642091fa0 100644 --- a/plugins/openpgp-api-lib/openpgp-api/src/main/java/org/openintents/openpgp/util/OpenPgpUtils.java +++ b/plugins/openpgp-api-lib/openpgp-api/src/main/java/org/openintents/openpgp/util/OpenPgpUtils.java @@ -80,7 +80,7 @@ public class OpenPgpUtils { } public static String extractClearsignedMessage(String text) { - if (!text.startsWith(PGP_MARKER_CLEARSIGN_BEGIN_MESSAGE)) { + if (text == null || !text.startsWith(PGP_MARKER_CLEARSIGN_BEGIN_MESSAGE)) { return null; } int endOfHeader = text.indexOf("\r\n\r\n") +4;