From a0a362a19dc612aa7c1c29c6e47b05b7c71be614 Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 14 Dec 2015 09:52:57 +0100 Subject: [PATCH 1/2] Rewrite message preview extraction --- .../message/preview/EncryptionDetector.java | 84 ++++++++ .../preview/MessagePreviewCreator.java | 51 +++++ .../k9/message/preview/PreviewResult.java | 51 +++++ .../message/preview/PreviewTextExtractor.java | 71 +++++++ .../k9/message/preview/TextPartFinder.java | 82 ++++++++ .../k9/message/MessageCreationHelper.java | 69 ++++++ .../preview/EncryptionDetectorTest.java | 116 ++++++++++ .../preview/MessagePreviewCreatorTest.java | 94 +++++++++ .../preview/PreviewTextExtractorTest.java | 162 ++++++++++++++ .../message/preview/TextPartFinderTest.java | 198 ++++++++++++++++++ 10 files changed, 978 insertions(+) create mode 100644 k9mail/src/main/java/com/fsck/k9/message/preview/EncryptionDetector.java create mode 100644 k9mail/src/main/java/com/fsck/k9/message/preview/MessagePreviewCreator.java create mode 100644 k9mail/src/main/java/com/fsck/k9/message/preview/PreviewResult.java create mode 100644 k9mail/src/main/java/com/fsck/k9/message/preview/PreviewTextExtractor.java create mode 100644 k9mail/src/main/java/com/fsck/k9/message/preview/TextPartFinder.java create mode 100644 k9mail/src/test/java/com/fsck/k9/message/MessageCreationHelper.java create mode 100644 k9mail/src/test/java/com/fsck/k9/message/preview/EncryptionDetectorTest.java create mode 100644 k9mail/src/test/java/com/fsck/k9/message/preview/MessagePreviewCreatorTest.java create mode 100644 k9mail/src/test/java/com/fsck/k9/message/preview/PreviewTextExtractorTest.java create mode 100644 k9mail/src/test/java/com/fsck/k9/message/preview/TextPartFinderTest.java diff --git a/k9mail/src/main/java/com/fsck/k9/message/preview/EncryptionDetector.java b/k9mail/src/main/java/com/fsck/k9/message/preview/EncryptionDetector.java new file mode 100644 index 000000000..f2a1c77e2 --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/message/preview/EncryptionDetector.java @@ -0,0 +1,84 @@ +package com.fsck.k9.message.preview; + + +import java.util.regex.Pattern; + +import android.support.annotation.NonNull; + +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.Part; +import com.fsck.k9.mail.internet.MessageExtractor; + +import static com.fsck.k9.mail.internet.MimeUtility.isSameMimeType; + + +class EncryptionDetector { + private static final Pattern PGP_MESSAGE_PATTERN = Pattern.compile( + "(^|\\r\\n)-----BEGIN PGP MESSAGE-----\\r\\n.*?\\r\\n-----END PGP MESSAGE-----(\\r\\n|$)", Pattern.DOTALL); + + + private final TextPartFinder textPartFinder; + + + EncryptionDetector(TextPartFinder textPartFinder) { + this.textPartFinder = textPartFinder; + } + + public boolean isEncrypted(@NonNull Message message) { + return isPgpMimeOrSMimeEncrypted(message) || containsInlinePgpEncryptedText(message); + } + + private boolean isPgpMimeOrSMimeEncrypted(Message message) { + return containsPartWithMimeType(message, "multipart/encrypted", "application/pkcs7-mime"); + } + + private boolean containsInlinePgpEncryptedText(Message message) { + Part textPart = textPartFinder.findFirstTextPart(message); + if (!isUsableTextPart(textPart)) { + return false; + } + + String text = MessageExtractor.getTextFromPart(textPart); + if (text == null) { + return false; + } + + return PGP_MESSAGE_PATTERN.matcher(text).find(); + } + + private boolean isUsableTextPart(Part textPart) { + return textPart != null && textPart.getBody() != null; + } + + private boolean containsPartWithMimeType(Part part, String... wantedMimeTypes) { + String mimeType = part.getMimeType(); + if (isMimeTypeAnyOf(mimeType, wantedMimeTypes)) { + return true; + } + + Body body = part.getBody(); + if (body instanceof Multipart) { + Multipart multipart = (Multipart) body; + for (BodyPart bodyPart : multipart.getBodyParts()) { + if (containsPartWithMimeType(bodyPart, wantedMimeTypes)) { + return true; + } + } + } + + return false; + } + + private boolean isMimeTypeAnyOf(String mimeType, String... wantedMimeTypes) { + for (String wantedMimeType : wantedMimeTypes) { + if (isSameMimeType(mimeType, wantedMimeType)) { + return true; + } + } + + return false; + } +} diff --git a/k9mail/src/main/java/com/fsck/k9/message/preview/MessagePreviewCreator.java b/k9mail/src/main/java/com/fsck/k9/message/preview/MessagePreviewCreator.java new file mode 100644 index 000000000..f91a773c6 --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/message/preview/MessagePreviewCreator.java @@ -0,0 +1,51 @@ +package com.fsck.k9.message.preview; + + +import android.support.annotation.NonNull; + +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.Part; + + +public class MessagePreviewCreator { + private final TextPartFinder textPartFinder; + private final PreviewTextExtractor previewTextExtractor; + private final EncryptionDetector encryptionDetector; + + + MessagePreviewCreator(TextPartFinder textPartFinder, PreviewTextExtractor previewTextExtractor, + EncryptionDetector encryptionDetector) { + this.textPartFinder = textPartFinder; + this.previewTextExtractor = previewTextExtractor; + this.encryptionDetector = encryptionDetector; + } + + public static MessagePreviewCreator newInstance() { + TextPartFinder textPartFinder = new TextPartFinder(); + PreviewTextExtractor previewTextExtractor = new PreviewTextExtractor(); + EncryptionDetector encryptionDetector = new EncryptionDetector(textPartFinder); + return new MessagePreviewCreator(textPartFinder, previewTextExtractor, encryptionDetector); + } + + public PreviewResult createPreview(@NonNull Message message) { + if (encryptionDetector.isEncrypted(message)) { + return PreviewResult.encrypted(); + } + + return extractText(message); + } + + private PreviewResult extractText(Message message) { + Part textPart = textPartFinder.findFirstTextPart(message); + if (textPart == null || hasEmptyBody(textPart)) { + return PreviewResult.none(); + } + + String previewText = previewTextExtractor.extractPreview(textPart); + return PreviewResult.text(previewText); + } + + private boolean hasEmptyBody(Part textPart) { + return textPart.getBody() == null; + } +} diff --git a/k9mail/src/main/java/com/fsck/k9/message/preview/PreviewResult.java b/k9mail/src/main/java/com/fsck/k9/message/preview/PreviewResult.java new file mode 100644 index 000000000..9874e7350 --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/message/preview/PreviewResult.java @@ -0,0 +1,51 @@ +package com.fsck.k9.message.preview; + + +import android.support.annotation.NonNull; + + +public class PreviewResult { + private final PreviewType previewType; + private final String previewText; + + + private PreviewResult(PreviewType previewType, String previewText) { + this.previewType = previewType; + this.previewText = previewText; + } + + public static PreviewResult text(@NonNull String previewText) { + return new PreviewResult(PreviewType.TEXT, previewText); + } + + public static PreviewResult encrypted() { + return new PreviewResult(PreviewType.ENCRYPTED, null); + } + + public static PreviewResult none() { + return new PreviewResult(PreviewType.NONE, null); + } + + public PreviewType getPreviewType() { + return previewType; + } + + public boolean isPreviewTextAvailable() { + return previewType == PreviewType.TEXT; + } + + public String getPreviewText() { + if (!isPreviewTextAvailable()) { + throw new IllegalStateException("Preview is not available"); + } + + return previewText; + } + + + public enum PreviewType { + NONE, + TEXT, + ENCRYPTED + } +} diff --git a/k9mail/src/main/java/com/fsck/k9/message/preview/PreviewTextExtractor.java b/k9mail/src/main/java/com/fsck/k9/message/preview/PreviewTextExtractor.java new file mode 100644 index 000000000..4871bf0e5 --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/message/preview/PreviewTextExtractor.java @@ -0,0 +1,71 @@ +package com.fsck.k9.message.preview; + + +import android.support.annotation.NonNull; + +import com.fsck.k9.helper.HtmlConverter; +import com.fsck.k9.mail.Part; +import com.fsck.k9.mail.internet.MessageExtractor; + +import static com.fsck.k9.mail.internet.MimeUtility.isSameMimeType; + + +class PreviewTextExtractor { + private static final int MAX_PREVIEW_LENGTH = 512; + private static final int MAX_CHARACTERS_CHECKED_FOR_PREVIEW = 8192; + + + public String extractPreview(@NonNull Part textPart) { + String text = MessageExtractor.getTextFromPart(textPart); + String plainText = convertFromHtmlIfNecessary(textPart, text); + + return stripTextForPreview(plainText); + } + + private String convertFromHtmlIfNecessary(Part textPart, String text) { + String mimeType = textPart.getMimeType(); + if (!isSameMimeType(mimeType, "text/html")) { + return text; + } + + return HtmlConverter.htmlToText(text); + } + + private String stripTextForPreview(String text) { + if (text == null) { + return ""; + } + + // Only look at the first 8k of a message when calculating + // the preview. This should avoid unnecessary + // memory usage on large messages + if (text.length() > MAX_CHARACTERS_CHECKED_FOR_PREVIEW) { + text = text.substring(0, MAX_CHARACTERS_CHECKED_FOR_PREVIEW); + } + + // Remove (correctly delimited by '-- \n') signatures + text = text.replaceAll("(?ms)^-- [\\r\\n]+.*", ""); + // try to remove lines of dashes in the preview + text = text.replaceAll("(?m)^----.*?$", ""); + // remove quoted text from the preview + text = text.replaceAll("(?m)^[#>].*$", ""); + // Remove a common quote header from the preview + text = text.replaceAll("(?m)^On .*wrote.?$", ""); + // Remove a more generic quote header from the preview + text = text.replaceAll("(?m)^.*\\w+:$", ""); + // Remove horizontal rules. + text = text.replaceAll("\\s*([-=_]{30,}+)\\s*", " "); + + // URLs in the preview should just be shown as "..." - They're not + // clickable and they usually overwhelm the preview + text = text.replaceAll("https?://\\S+", "..."); + // Don't show newlines in the preview + text = text.replaceAll("(\\r|\\n)+", " "); + // Collapse whitespace in the preview + text = text.replaceAll("\\s+", " "); + // Remove any whitespace at the beginning and end of the string. + text = text.trim(); + + return (text.length() > MAX_PREVIEW_LENGTH) ? text.substring(0, MAX_PREVIEW_LENGTH - 1) + "…" : text; + } +} diff --git a/k9mail/src/main/java/com/fsck/k9/message/preview/TextPartFinder.java b/k9mail/src/main/java/com/fsck/k9/message/preview/TextPartFinder.java new file mode 100644 index 000000000..98d647b84 --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/message/preview/TextPartFinder.java @@ -0,0 +1,82 @@ +package com.fsck.k9.message.preview; + + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.fsck.k9.mail.Body; +import com.fsck.k9.mail.BodyPart; +import com.fsck.k9.mail.Multipart; +import com.fsck.k9.mail.Part; + +import static com.fsck.k9.mail.internet.MimeUtility.isSameMimeType; + + +class TextPartFinder { + @Nullable + public Part findFirstTextPart(@NonNull Part part) { + String mimeType = part.getMimeType(); + Body body = part.getBody(); + + if (body instanceof Multipart) { + Multipart multipart = (Multipart) body; + if (isSameMimeType(mimeType, "multipart/alternative")) { + return findTextPartInMultipartAlternative(multipart); + } else { + return findTextPartInMultipart(multipart); + } + } else if (isSameMimeType(mimeType, "text/plain") || isSameMimeType(mimeType, "text/html")) { + return part; + } + + return null; + } + + private Part findTextPartInMultipartAlternative(Multipart multipart) { + Part htmlPart = null; + + for (BodyPart bodyPart : multipart.getBodyParts()) { + String mimeType = bodyPart.getMimeType(); + Body body = bodyPart.getBody(); + + if (body instanceof Multipart) { + Part candidatePart = findFirstTextPart(bodyPart); + if (candidatePart != null) { + if (isSameMimeType(candidatePart.getMimeType(), "text/html")) { + htmlPart = candidatePart; + } else { + return candidatePart; + } + } + } else if (isSameMimeType(mimeType, "text/plain")) { + return bodyPart; + } else if (isSameMimeType(mimeType, "text/html") && htmlPart == null) { + htmlPart = bodyPart; + } + } + + if (htmlPart != null) { + return htmlPart; + } + + return null; + } + + private Part findTextPartInMultipart(Multipart multipart) { + for (BodyPart bodyPart : multipart.getBodyParts()) { + String mimeType = bodyPart.getMimeType(); + Body body = bodyPart.getBody(); + + if (body instanceof Multipart) { + Part candidatePart = findFirstTextPart(bodyPart); + if (candidatePart != null) { + return candidatePart; + } + } else if (isSameMimeType(mimeType, "text/plain") || isSameMimeType(mimeType, "text/html")) { + return bodyPart; + } + } + + return null; + } +} diff --git a/k9mail/src/test/java/com/fsck/k9/message/MessageCreationHelper.java b/k9mail/src/test/java/com/fsck/k9/message/MessageCreationHelper.java new file mode 100644 index 000000000..26de0d465 --- /dev/null +++ b/k9mail/src/test/java/com/fsck/k9/message/MessageCreationHelper.java @@ -0,0 +1,69 @@ +package com.fsck.k9.message; + + +import com.fsck.k9.mail.Body; +import com.fsck.k9.mail.BodyPart; +import com.fsck.k9.mail.Message; +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.MimeMultipart; +import com.fsck.k9.mail.internet.TextBody; +import com.fsck.k9.mailstore.BinaryMemoryBody; + + +public class MessageCreationHelper { + public static BodyPart createTextPart(String mimeType) throws MessagingException { + return createTextPart(mimeType, ""); + } + + public static BodyPart createTextPart(String mimeType, String text) throws MessagingException { + TextBody body = new TextBody(text); + return new MimeBodyPart(body, mimeType); + } + + public static BodyPart createEmptyPart(String mimeType) throws MessagingException { + return new MimeBodyPart(null, mimeType); + } + + public static BodyPart createPart(String mimeType) throws MessagingException { + BinaryMemoryBody body = new BinaryMemoryBody(new byte[0], "utf-8"); + return new MimeBodyPart(body, mimeType); + } + + public static BodyPart createMultipart(String mimeType, BodyPart... parts) throws MessagingException { + MimeMultipart multipart = createMultipartBody(mimeType, parts); + return new MimeBodyPart(multipart, mimeType); + } + + public static Message createTextMessage(String mimeType, String text) throws MessagingException { + TextBody body = new TextBody(text); + return createMessage(mimeType, body); + } + + public static Message createMultipartMessage(String mimeType, BodyPart... parts) throws MessagingException { + MimeMultipart body = createMultipartBody(mimeType, parts); + return createMessage(mimeType, body); + } + + public static Message createMessage(String mimeType) throws MessagingException { + return createMessage(mimeType, null); + } + + private static Message createMessage(String mimeType, Body body) throws MessagingException { + MimeMessage message = new MimeMessage(); + message.setBody(body); + message.setHeader(MimeHeader.HEADER_CONTENT_TYPE, mimeType); + + return message; + } + + private static MimeMultipart createMultipartBody(String mimeType, BodyPart[] parts) throws MessagingException { + MimeMultipart multipart = new MimeMultipart(mimeType, "boundary"); + for (BodyPart part : parts) { + multipart.addBodyPart(part); + } + return multipart; + } +} diff --git a/k9mail/src/test/java/com/fsck/k9/message/preview/EncryptionDetectorTest.java b/k9mail/src/test/java/com/fsck/k9/message/preview/EncryptionDetectorTest.java new file mode 100644 index 000000000..c9f46336b --- /dev/null +++ b/k9mail/src/test/java/com/fsck/k9/message/preview/EncryptionDetectorTest.java @@ -0,0 +1,116 @@ +package com.fsck.k9.message.preview; + + +import com.fsck.k9.mail.Message; +import org.junit.Before; +import org.junit.Test; + +import static com.fsck.k9.message.MessageCreationHelper.createMessage; +import static com.fsck.k9.message.MessageCreationHelper.createMultipartMessage; +import static com.fsck.k9.message.MessageCreationHelper.createPart; +import static com.fsck.k9.message.MessageCreationHelper.createTextMessage; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + + +public class EncryptionDetectorTest { + private static final String CRLF = "\r\n"; + + + private TextPartFinder textPartFinder; + private EncryptionDetector encryptionDetector; + + + @Before + public void setUp() throws Exception { + textPartFinder = mock(TextPartFinder.class); + + encryptionDetector = new EncryptionDetector(textPartFinder); + } + + @Test + public void isEncrypted_withTextPlain_shouldReturnFalse() throws Exception { + Message message = createTextMessage("text/plain", "plain text"); + + boolean encrypted = encryptionDetector.isEncrypted(message); + + assertFalse(encrypted); + } + + @Test + public void isEncrypted_withMultipartEncrypted_shouldReturnTrue() throws Exception { + Message message = createMultipartMessage("multipart/encrypted", + createPart("application/octet-stream"), createPart("application/octet-stream")); + + boolean encrypted = encryptionDetector.isEncrypted(message); + + assertTrue(encrypted); + } + + @Test + public void isEncrypted_withSMimePart_shouldReturnTrue() throws Exception { + Message message = createMessage("application/pkcs7-mime"); + + boolean encrypted = encryptionDetector.isEncrypted(message); + + assertTrue(encrypted); + } + + @Test + public void isEncrypted_withMultipartMixedContainingSMimePart_shouldReturnTrue() throws Exception { + Message message = createMultipartMessage("multipart/mixed", + createPart("application/pkcs7-mime"), createPart("text/plain")); + + boolean encrypted = encryptionDetector.isEncrypted(message); + + assertTrue(encrypted); + } + + @Test + public void isEncrypted_withInlinePgp_shouldReturnTrue() throws Exception { + Message message = createTextMessage("text/plain", "" + + "-----BEGIN PGP MESSAGE-----" + CRLF + + "some encrypted stuff here" + CRLF + + "-----END PGP MESSAGE-----"); + when(textPartFinder.findFirstTextPart(message)).thenReturn(message); + + boolean encrypted = encryptionDetector.isEncrypted(message); + + assertTrue(encrypted); + } + + @Test + public void isEncrypted_withPlainTextAndInlinePgp_shouldReturnTrue() throws Exception { + Message message = createTextMessage("text/plain", "" + + "preamble" + CRLF + + "-----BEGIN PGP MESSAGE-----" + CRLF + + "some encrypted stuff here" + CRLF + + "-----END PGP MESSAGE-----" + CRLF + + "epilogue"); + when(textPartFinder.findFirstTextPart(message)).thenReturn(message); + + boolean encrypted = encryptionDetector.isEncrypted(message); + + assertTrue(encrypted); + } + + @Test + public void isEncrypted_withQuotedInlinePgp_shouldReturnFalse() throws Exception { + Message message = createTextMessage("text/plain", "" + + "good talk!" + CRLF + + CRLF + + "> -----BEGIN PGP MESSAGE-----" + CRLF + + "> some encrypted stuff here" + CRLF + + "> -----END PGP MESSAGE-----" + CRLF + + CRLF + + "-- " + CRLF + + "my signature"); + when(textPartFinder.findFirstTextPart(message)).thenReturn(message); + + boolean encrypted = encryptionDetector.isEncrypted(message); + + assertFalse(encrypted); + } +} diff --git a/k9mail/src/test/java/com/fsck/k9/message/preview/MessagePreviewCreatorTest.java b/k9mail/src/test/java/com/fsck/k9/message/preview/MessagePreviewCreatorTest.java new file mode 100644 index 000000000..bf4cde1f1 --- /dev/null +++ b/k9mail/src/test/java/com/fsck/k9/message/preview/MessagePreviewCreatorTest.java @@ -0,0 +1,94 @@ +package com.fsck.k9.message.preview; + + +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.Part; +import com.fsck.k9.mail.internet.MimeMessage; +import com.fsck.k9.message.preview.PreviewResult.PreviewType; +import org.junit.Before; +import org.junit.Test; + +import static com.fsck.k9.message.MessageCreationHelper.createEmptyPart; +import static com.fsck.k9.message.MessageCreationHelper.createTextPart; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + + +public class MessagePreviewCreatorTest { + private TextPartFinder textPartFinder; + private PreviewTextExtractor previewTextExtractor; + private EncryptionDetector encryptionDetector; + private MessagePreviewCreator previewCreator; + + @Before + public void setUp() throws Exception { + textPartFinder = mock(TextPartFinder.class); + previewTextExtractor = mock(PreviewTextExtractor.class); + encryptionDetector = mock(EncryptionDetector.class); + + previewCreator = new MessagePreviewCreator(textPartFinder, previewTextExtractor, encryptionDetector); + } + + @Test + public void createPreview_withEncryptedMessage() throws Exception { + Message message = createDummyMessage(); + when(encryptionDetector.isEncrypted(message)).thenReturn(true); + + PreviewResult result = previewCreator.createPreview(message); + + assertFalse(result.isPreviewTextAvailable()); + assertEquals(PreviewType.ENCRYPTED, result.getPreviewType()); + verifyNoMoreInteractions(textPartFinder); + verifyNoMoreInteractions(previewTextExtractor); + } + + @Test + public void createPreview_withoutTextPart() throws Exception { + Message message = createDummyMessage(); + when(encryptionDetector.isEncrypted(message)).thenReturn(false); + when(textPartFinder.findFirstTextPart(message)).thenReturn(null); + + PreviewResult result = previewCreator.createPreview(message); + + assertFalse(result.isPreviewTextAvailable()); + assertEquals(PreviewType.NONE, result.getPreviewType()); + verifyNoMoreInteractions(previewTextExtractor); + } + + @Test + public void createPreview_withEmptyTextPart() throws Exception { + Message message = createDummyMessage(); + Part textPart = createEmptyPart("text/plain"); + when(encryptionDetector.isEncrypted(message)).thenReturn(false); + when(textPartFinder.findFirstTextPart(message)).thenReturn(textPart); + + PreviewResult result = previewCreator.createPreview(message); + + assertFalse(result.isPreviewTextAvailable()); + assertEquals(PreviewType.NONE, result.getPreviewType()); + verifyNoMoreInteractions(previewTextExtractor); + } + + @Test + public void createPreview_withTextPart() throws Exception { + Message message = createDummyMessage(); + Part textPart = createTextPart("text/plain"); + when(encryptionDetector.isEncrypted(message)).thenReturn(false); + when(textPartFinder.findFirstTextPart(message)).thenReturn(textPart); + when(previewTextExtractor.extractPreview(textPart)).thenReturn("expected"); + + PreviewResult result = previewCreator.createPreview(message); + + assertTrue(result.isPreviewTextAvailable()); + assertEquals(PreviewType.TEXT, result.getPreviewType()); + assertEquals("expected", result.getPreviewText()); + } + + private Message createDummyMessage() { + return new MimeMessage(); + } +} diff --git a/k9mail/src/test/java/com/fsck/k9/message/preview/PreviewTextExtractorTest.java b/k9mail/src/test/java/com/fsck/k9/message/preview/PreviewTextExtractorTest.java new file mode 100644 index 000000000..78391e445 --- /dev/null +++ b/k9mail/src/test/java/com/fsck/k9/message/preview/PreviewTextExtractorTest.java @@ -0,0 +1,162 @@ +package com.fsck.k9.message.preview; + + +import com.fsck.k9.mail.Part; +import com.fsck.k9.mail.internet.MimeBodyPart; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static com.fsck.k9.message.MessageCreationHelper.createTextPart; +import static org.junit.Assert.assertEquals; + + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class PreviewTextExtractorTest { + private PreviewTextExtractor previewTextExtractor; + + + @Before + public void setUp() throws Exception { + previewTextExtractor = new PreviewTextExtractor(); + } + + @Test + public void extractPreview_withEmptyBody() throws Exception { + Part part = new MimeBodyPart(null, "text/plain"); + + //TODO: throw exception + String preview = previewTextExtractor.extractPreview(part); + + assertEquals("", preview); + } + + @Test + public void extractPreview_withSimpleTextPlain() throws Exception { + String text = "The quick brown fox jumps over the lazy dog"; + Part part = createTextPart("text/plain", text); + + String preview = previewTextExtractor.extractPreview(part); + + assertEquals(text, preview); + } + + @Test + public void extractPreview_withSimpleTextHtml() throws Exception { + String text = "The quick brown fox jumps over the lazy dog"; + Part part = createTextPart("text/html", text); + + String preview = previewTextExtractor.extractPreview(part); + + assertEquals("The quick brown fox jumps over the lazy dog", preview); + } + + @Test + public void extractPreview_withLongTextPlain() throws Exception { + String text = "" + + "10--------20--------30--------40--------50--------" + + "60--------70--------80--------90--------100-------" + + "110-------120-------130-------140-------150-------" + + "160-------170-------180-------190-------200-------" + + "210-------220-------230-------240-------250-------" + + "260-------270-------280-------290-------300-------" + + "310-------320-------330-------340-------350-------" + + "360-------370-------380-------390-------400-------" + + "410-------420-------430-------440-------450-------" + + "460-------470-------480-------490-------500-------" + + "510-------520-------"; + Part part = createTextPart("text/plain", text); + + String preview = previewTextExtractor.extractPreview(part); + + assertEquals(text.substring(0, 511) + "…", preview); + } + + @Test + public void extractPreview_shouldStripSignature() throws Exception { + String text = "" + + "Some text\r\n" + + "-- \r\n" + + "Signature"; + Part part = createTextPart("text/plain", text); + + String preview = previewTextExtractor.extractPreview(part); + + assertEquals("Some text", preview); + } + + @Test + public void extractPreview_shouldStripHorizontalLine() throws Exception { + String text = "" + + "line 1\r\n" + + "----\r\n" + + "line 2"; + Part part = createTextPart("text/plain", text); + + String preview = previewTextExtractor.extractPreview(part); + + assertEquals("line 1 line 2", preview); + } + + @Test + public void extractPreview_shouldStripQuoteHeaderAndQuotedText() throws Exception { + String text = "" + + "some text\r\n" + + "On 01/02/03 someone wrote\r\n" + + "> some quoted text\r\n" + + "# some other quoted text\r\n"; + Part part = createTextPart("text/plain", text); + + String preview = previewTextExtractor.extractPreview(part); + + assertEquals("some text", preview); + } + + @Test + public void extractPreview_shouldStripGenericQuoteHeader() throws Exception { + String text = "" + + "Am 13.12.2015 um 23:42 schrieb Hans:\r\n" + + "> hallo\r\n" + + "hi there\r\n"; + Part part = createTextPart("text/plain", text); + + String preview = previewTextExtractor.extractPreview(part); + + assertEquals("hi there", preview); + } + + @Test + public void extractPreview_shouldStripHorizontalRules() throws Exception { + String text = "line 1" + + "------------------------------\r\n" + + "line 2"; + Part part = createTextPart("text/plain", text); + + String preview = previewTextExtractor.extractPreview(part); + + assertEquals("line 1 line 2", preview); + } + + @Test + public void extractPreview_shouldReplaceUrl() throws Exception { + String text = "some url: https://k9mail.org/"; + Part part = createTextPart("text/plain", text); + + String preview = previewTextExtractor.extractPreview(part); + + assertEquals("some url: ...", preview); + } + + @Test + public void extractPreview_shouldCollapseAndTrimWhitespace() throws Exception { + String text = " whitespace is\t\tfun "; + Part part = createTextPart("text/plain", text); + + String preview = previewTextExtractor.extractPreview(part); + + assertEquals("whitespace is fun", preview); + } +} diff --git a/k9mail/src/test/java/com/fsck/k9/message/preview/TextPartFinderTest.java b/k9mail/src/test/java/com/fsck/k9/message/preview/TextPartFinderTest.java new file mode 100644 index 000000000..56f898a9b --- /dev/null +++ b/k9mail/src/test/java/com/fsck/k9/message/preview/TextPartFinderTest.java @@ -0,0 +1,198 @@ +package com.fsck.k9.message.preview; + + +import com.fsck.k9.mail.BodyPart; +import com.fsck.k9.mail.Part; +import org.junit.Before; +import org.junit.Test; + +import static com.fsck.k9.message.MessageCreationHelper.createEmptyPart; +import static com.fsck.k9.message.MessageCreationHelper.createMultipart; +import static com.fsck.k9.message.MessageCreationHelper.createPart; +import static com.fsck.k9.message.MessageCreationHelper.createTextPart; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + + +public class TextPartFinderTest { + private TextPartFinder textPartFinder; + + + @Before + public void setUp() throws Exception { + textPartFinder = new TextPartFinder(); + } + + @Test + public void findFirstTextPart_withTextPlainPart() throws Exception { + Part part = createTextPart("text/plain"); + + Part result = textPartFinder.findFirstTextPart(part); + + assertEquals(part, result); + } + + @Test + public void findFirstTextPart_withTextHtmlPart() throws Exception { + Part part = createTextPart("text/html"); + + Part result = textPartFinder.findFirstTextPart(part); + + assertEquals(part, result); + } + + @Test + public void findFirstTextPart_withoutTextPart() throws Exception { + Part part = createPart("image/jpeg"); + + Part result = textPartFinder.findFirstTextPart(part); + + assertNull(result); + } + + @Test + public void findFirstTextPart_withMultipartAlternative() throws Exception { + BodyPart expected = createTextPart("text/plain"); + Part part = createMultipart("multipart/alternative", expected, createTextPart("text/html")); + + Part result = textPartFinder.findFirstTextPart(part); + + assertEquals(expected, result); + } + + @Test + public void findFirstTextPart_withMultipartAlternativeHtmlPartFirst() throws Exception { + BodyPart expected = createTextPart("text/plain"); + Part part = createMultipart("multipart/alternative", createTextPart("text/html"), expected); + + Part result = textPartFinder.findFirstTextPart(part); + + assertEquals(expected, result); + } + + @Test + public void findFirstTextPart_withMultipartAlternativeContainingOnlyTextHtmlPart() throws Exception { + BodyPart expected = createTextPart("text/html"); + Part part = createMultipart("multipart/alternative", + createPart("image/gif"), + expected, + createTextPart("text/html")); + + Part result = textPartFinder.findFirstTextPart(part); + + assertEquals(expected, result); + } + + @Test + public void findFirstTextPart_withMultipartAlternativeNotContainingTextPart() throws Exception { + Part part = createMultipart("multipart/alternative", + createPart("image/gif"), + createPart("application/pdf")); + + Part result = textPartFinder.findFirstTextPart(part); + + assertNull(result); + } + + @Test + public void findFirstTextPart_withMultipartAlternativeContainingMultipartRelatedContainingTextPlain() + throws Exception { + BodyPart expected = createTextPart("text/plain"); + Part part = createMultipart("multipart/alternative", + createMultipart("multipart/related", expected, createPart("image/jpeg")), + createTextPart("text/html")); + + Part result = textPartFinder.findFirstTextPart(part); + + assertEquals(expected, result); + } + + @Test + public void findFirstTextPart_withMultipartAlternativeContainingMultipartRelatedContainingTextHtmlFirst() + throws Exception { + BodyPart expected = createTextPart("text/plain"); + Part part = createMultipart("multipart/alternative", + createMultipart("multipart/related", createTextPart("text/html"), createPart("image/jpeg")), + expected); + + Part result = textPartFinder.findFirstTextPart(part); + + assertEquals(expected, result); + } + + @Test + public void findFirstTextPart_withMultipartMixedContainingTextPlain() throws Exception { + BodyPart expected = createTextPart("text/plain"); + Part part = createMultipart("multipart/mixed", createPart("image/jpeg"), expected); + + Part result = textPartFinder.findFirstTextPart(part); + + assertEquals(expected, result); + } + + @Test + public void findFirstTextPart_withMultipartMixedContainingTextHtmlFirst() throws Exception { + BodyPart expected = createTextPart("text/html"); + Part part = createMultipart("multipart/mixed", expected, createTextPart("text/plain")); + + Part result = textPartFinder.findFirstTextPart(part); + + assertEquals(expected, result); + } + + @Test + public void findFirstTextPart_withMultipartMixedNotContainingTextPart() throws Exception { + Part part = createMultipart("multipart/mixed", createPart("image/jpeg"), createPart("image/gif")); + + Part result = textPartFinder.findFirstTextPart(part); + + assertNull(result); + } + + @Test + public void findFirstTextPart_withMultipartMixedContainingMultipartAlternative() throws Exception { + BodyPart expected = createTextPart("text/plain"); + Part part = createMultipart("multipart/mixed", + createPart("image/jpeg"), + createMultipart("multipart/alternative", expected, createTextPart("text/html")), + createTextPart("text/plain")); + + Part result = textPartFinder.findFirstTextPart(part); + + assertEquals(expected, result); + } + + @Test + public void findFirstTextPart_withMultipartMixedContainingMultipartAlternativeWithTextPlainPartLast() + throws Exception { + BodyPart expected = createTextPart("text/plain"); + Part part = createMultipart("multipart/mixed", + createMultipart("multipart/alternative", createTextPart("text/html"), expected)); + + Part result = textPartFinder.findFirstTextPart(part); + + assertEquals(expected, result); + } + + @Test + public void findFirstTextPart_withMultipartAlternativeContainingEmptyTextPlainPart() + throws Exception { + BodyPart expected = createEmptyPart("text/plain"); + Part part = createMultipart("multipart/alternative", expected, createTextPart("text/html")); + + Part result = textPartFinder.findFirstTextPart(part); + + assertEquals(expected, result); + } + + @Test + public void findFirstTextPart_withMultipartMixedContainingEmptyTextHtmlPart() + throws Exception { + BodyPart expected = createEmptyPart("text/html"); + Part part = createMultipart("multipart/mixed", expected, createTextPart("text/plain")); + + Part result = textPartFinder.findFirstTextPart(part); + + assertEquals(expected, result); + } +} From d6b4452ade0bf76c9b58dc94c9094b933c38975d Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 16 Dec 2015 15:10:47 +0100 Subject: [PATCH 2/2] Display "*Encrypted*" in preview of encrypted messages --- .../main/java/com/fsck/k9/mail/Message.java | 1 - .../fsck/k9/mail/internet/MimeMessage.java | 5 - .../fsck/k9/fragment/MessageListFragment.java | 38 ++++- .../fsck/k9/mailstore/AttachmentCounter.java | 20 +++ .../k9/mailstore/DatabasePreviewType.java | 49 ++++++ .../com/fsck/k9/mailstore/LocalFolder.java | 21 ++- .../com/fsck/k9/mailstore/LocalMessage.java | 18 +- .../com/fsck/k9/mailstore/LocalStore.java | 18 +- .../k9/mailstore/MessageInfoExtractor.java | 43 ----- .../k9/mailstore/MessagePreviewExtractor.java | 154 ------------------ .../k9/mailstore/StoreSchemaDefinition.java | 9 + .../NotificationContentCreator.java | 21 ++- .../com/fsck/k9/provider/EmailProvider.java | 1 + k9mail/src/main/res/values/strings.xml | 1 + .../NotificationContentCreatorTest.java | 16 ++ 15 files changed, 191 insertions(+), 224 deletions(-) create mode 100644 k9mail/src/main/java/com/fsck/k9/mailstore/AttachmentCounter.java create mode 100644 k9mail/src/main/java/com/fsck/k9/mailstore/DatabasePreviewType.java delete mode 100644 k9mail/src/main/java/com/fsck/k9/mailstore/MessageInfoExtractor.java delete mode 100644 k9mail/src/main/java/com/fsck/k9/mailstore/MessagePreviewExtractor.java diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java b/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java index 67ba3a244..3d19cbf9d 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/Message.java @@ -142,7 +142,6 @@ public abstract class Message implements Part, CompositeBody { public abstract long getId(); - public abstract String getPreview(); public abstract boolean hasAttachments(); public abstract int getSize(); diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java index af42744d6..46dc6f89d 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/internet/MimeMessage.java @@ -656,11 +656,6 @@ public class MimeMessage extends Message { return Long.parseLong(mUid); //or maybe .mMessageId? } - @Override - public String getPreview() { - return ""; - } - @Override public boolean hasAttachments() { return false; diff --git a/k9mail/src/main/java/com/fsck/k9/fragment/MessageListFragment.java b/k9mail/src/main/java/com/fsck/k9/fragment/MessageListFragment.java index cf015ebec..fa58e0dd6 100644 --- a/k9mail/src/main/java/com/fsck/k9/fragment/MessageListFragment.java +++ b/k9mail/src/main/java/com/fsck/k9/fragment/MessageListFragment.java @@ -88,6 +88,7 @@ import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Folder; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mailstore.DatabasePreviewType; import com.fsck.k9.mailstore.LocalFolder; import com.fsck.k9.mailstore.LocalMessage; import com.fsck.k9.mailstore.LocalStore; @@ -125,6 +126,7 @@ public class MessageListFragment extends Fragment implements OnItemClickListener MessageColumns.FORWARDED, MessageColumns.ATTACHMENT_COUNT, MessageColumns.FOLDER_ID, + MessageColumns.PREVIEW_TYPE, MessageColumns.PREVIEW, ThreadColumns.ROOT, SpecialColumns.ACCOUNT_UUID, @@ -147,11 +149,12 @@ public class MessageListFragment extends Fragment implements OnItemClickListener private static final int FORWARDED_COLUMN = 11; private static final int ATTACHMENT_COUNT_COLUMN = 12; private static final int FOLDER_ID_COLUMN = 13; - private static final int PREVIEW_COLUMN = 14; - private static final int THREAD_ROOT_COLUMN = 15; - private static final int ACCOUNT_UUID_COLUMN = 16; - private static final int FOLDER_NAME_COLUMN = 17; - private static final int THREAD_COUNT_COLUMN = 18; + private static final int PREVIEW_TYPE_COLUMN = 14; + private static final int PREVIEW_COLUMN = 15; + private static final int THREAD_ROOT_COLUMN = 16; + private static final int ACCOUNT_UUID_COLUMN = 17; + private static final int FOLDER_NAME_COLUMN = 18; + private static final int THREAD_COUNT_COLUMN = 19; private static final String[] PROJECTION = Arrays.copyOf(THREADED_PROJECTION, THREAD_COUNT_COLUMN); @@ -2029,10 +2032,8 @@ public class MessageListFragment extends Fragment implements OnItemClickListener .append(beforePreviewText); if (mPreviewLines > 0) { - String preview = cursor.getString(PREVIEW_COLUMN); - if (preview != null) { - messageStringBuilder.append(" ").append(preview); - } + String preview = getPreview(cursor); + messageStringBuilder.append(" ").append(preview); } holder.preview.setText(messageStringBuilder, TextView.BufferType.SPANNABLE); @@ -2096,6 +2097,25 @@ public class MessageListFragment extends Fragment implements OnItemClickListener holder.date.setText(displayDate); } + + private String getPreview(Cursor cursor) { + String previewTypeString = cursor.getString(PREVIEW_TYPE_COLUMN); + DatabasePreviewType previewType = DatabasePreviewType.fromDatabaseValue(previewTypeString); + + switch (previewType) { + case NONE: { + return ""; + } + case ENCRYPTED: { + return getString(R.string.preview_encrypted); + } + case TEXT: { + return cursor.getString(PREVIEW_COLUMN); + } + } + + throw new AssertionError("Unknown preview type: " + previewType); + } } class MessageViewHolder implements View.OnClickListener { diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/AttachmentCounter.java b/k9mail/src/main/java/com/fsck/k9/mailstore/AttachmentCounter.java new file mode 100644 index 000000000..d093567bf --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/AttachmentCounter.java @@ -0,0 +1,20 @@ +package com.fsck.k9.mailstore; + + +import java.util.ArrayList; +import java.util.List; + +import com.fsck.k9.mail.Message; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.Part; +import com.fsck.k9.mail.internet.MessageExtractor; + + +class AttachmentCounter { + public int getAttachmentCount(Message message) throws MessagingException { + List attachments = new ArrayList(); + MessageExtractor.getViewables(message, attachments); + + return attachments.size(); + } +} diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/DatabasePreviewType.java b/k9mail/src/main/java/com/fsck/k9/mailstore/DatabasePreviewType.java new file mode 100644 index 000000000..a2d51558e --- /dev/null +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/DatabasePreviewType.java @@ -0,0 +1,49 @@ +package com.fsck.k9.mailstore; + + +import com.fsck.k9.message.preview.PreviewResult.PreviewType; + + +public enum DatabasePreviewType { + NONE("none", PreviewType.NONE), + TEXT("text", PreviewType.TEXT), + ENCRYPTED("encrypted", PreviewType.ENCRYPTED); + + + private final String databaseValue; + private final PreviewType previewType; + + + DatabasePreviewType(String databaseValue, PreviewType previewType) { + this.databaseValue = databaseValue; + this.previewType = previewType; + } + + public static DatabasePreviewType fromDatabaseValue(String databaseValue) { + for (DatabasePreviewType databasePreviewType : values()) { + if (databasePreviewType.getDatabaseValue().equals(databaseValue)) { + return databasePreviewType; + } + } + + throw new AssertionError("Unknown database value: " + databaseValue); + } + + public static DatabasePreviewType fromPreviewType(PreviewType previewType) { + for (DatabasePreviewType databasePreviewType : values()) { + if (databasePreviewType.previewType == previewType) { + return databasePreviewType; + } + } + + throw new AssertionError("Unknown preview type: " + previewType); + } + + public String getDatabaseValue() { + return databaseValue; + } + + public PreviewType getPreviewType() { + return previewType; + } +} diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java index e2d06dcf4..8fa3354e1 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalFolder.java @@ -54,6 +54,9 @@ import com.fsck.k9.mail.internet.SizeAware; import com.fsck.k9.mail.message.MessageHeaderParser; import com.fsck.k9.mailstore.LockableDatabase.DbCallback; import com.fsck.k9.mailstore.LockableDatabase.WrappedException; +import com.fsck.k9.message.preview.MessagePreviewCreator; +import com.fsck.k9.message.preview.PreviewResult; +import com.fsck.k9.message.preview.PreviewResult.PreviewType; import org.apache.commons.io.IOUtils; import org.apache.james.mime4j.util.MimeUtil; @@ -1233,9 +1236,13 @@ public class LocalFolder extends Folder implements Serializable { } try { - MessageInfoExtractor messageExtractor = new MessageInfoExtractor(localStore.context, message); - String preview = messageExtractor.getMessageTextPreview(); - int attachmentCount = messageExtractor.getAttachmentCount(); + MessagePreviewCreator previewCreator = localStore.getMessagePreviewCreator(); + PreviewResult previewResult = previewCreator.createPreview(message); + PreviewType previewType = previewResult.getPreviewType(); + DatabasePreviewType databasePreviewType = DatabasePreviewType.fromPreviewType(previewType); + + AttachmentCounter attachmentCounter = localStore.getAttachmentCounter(); + int attachmentCount = attachmentCounter.getAttachmentCount(message); long rootMessagePartId = saveMessageParts(db, message); @@ -1256,7 +1263,6 @@ public class LocalFolder extends Folder implements Serializable { cv.put("to_list", Address.pack(message.getRecipients(RecipientType.TO))); cv.put("cc_list", Address.pack(message.getRecipients(RecipientType.CC))); cv.put("bcc_list", Address.pack(message.getRecipients(RecipientType.BCC))); - cv.put("preview", preview); cv.put("reply_to_list", Address.pack(message.getReplyTo())); cv.put("attachment_count", attachmentCount); cv.put("internal_date", message.getInternalDate() == null @@ -1264,6 +1270,13 @@ public class LocalFolder extends Folder implements Serializable { cv.put("mime_type", message.getMimeType()); cv.put("empty", 0); + cv.put("preview_type", databasePreviewType.getDatabaseValue()); + if (previewResult.isPreviewTextAvailable()) { + cv.put("preview", previewResult.getPreviewText()); + } else { + cv.putNull("preview"); + } + String messageId = message.getMessageId(); if (messageId != null) { cv.put("message_id", messageId); diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java index 8291692e8..3952dce97 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalMessage.java @@ -20,6 +20,8 @@ import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.internet.MimeMessage; import com.fsck.k9.mailstore.LockableDatabase.DbCallback; import com.fsck.k9.mailstore.LockableDatabase.WrappedException; +import com.fsck.k9.message.preview.PreviewResult.PreviewType; + public class LocalMessage extends MimeMessage { protected MessageReference mReference; @@ -37,6 +39,7 @@ public class LocalMessage extends MimeMessage { private long mRootId; private long messagePartId; private String mimeType; + private PreviewType previewType; private LocalMessage(LocalStore localStore) { this.localStore = localStore; @@ -84,8 +87,14 @@ public class LocalMessage extends MimeMessage { this.setInternalDate(new Date(cursor.getLong(11))); this.setMessageId(cursor.getString(12)); - final String preview = cursor.getString(14); - mPreview = (preview == null ? "" : preview); + String previewTypeString = cursor.getString(24); + DatabasePreviewType databasePreviewType = DatabasePreviewType.fromDatabaseValue(previewTypeString); + previewType = databasePreviewType.getPreviewType(); + if (previewType == PreviewType.TEXT) { + mPreview = cursor.getString(14); + } else { + mPreview = ""; + } if (this.mFolder == null) { LocalFolder f = new LocalFolder(this.localStore, cursor.getInt(13)); @@ -134,7 +143,10 @@ public class LocalMessage extends MimeMessage { super.writeTo(out); } - @Override + public PreviewType getPreviewType() { + return previewType; + } + public String getPreview() { return mPreview; } diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java index 0f597817b..fef4faf66 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/LocalStore.java @@ -26,6 +26,7 @@ import com.fsck.k9.mailstore.LocalFolder.MoreMessages; import com.fsck.k9.mailstore.StorageManager.StorageProvider; import com.fsck.k9.mailstore.LockableDatabase.DbCallback; import com.fsck.k9.mailstore.LockableDatabase.WrappedException; +import com.fsck.k9.message.preview.MessagePreviewCreator; import com.fsck.k9.provider.EmailProvider; import com.fsck.k9.provider.EmailProvider.MessageColumns; import com.fsck.k9.search.LocalSearch; @@ -85,7 +86,7 @@ public class LocalStore extends Store implements Serializable { "subject, sender_list, date, uid, flags, messages.id, to_list, cc_list, " + "bcc_list, reply_to_list, attachment_count, internal_date, messages.message_id, " + "folder_id, preview, threads.id, threads.root, deleted, read, flagged, answered, " + - "forwarded, message_part_id, mime_type "; + "forwarded, message_part_id, mime_type, preview_type "; static final String GET_FOLDER_COLS = "folders.id, name, visible_limit, last_updated, status, push_state, last_pushed, " + @@ -129,7 +130,7 @@ public class LocalStore extends Store implements Serializable { */ private static final int THREAD_FLAG_UPDATE_BATCH_SIZE = 500; - public static final int DB_VERSION = 53; + public static final int DB_VERSION = 54; public static String getColumnNameForFlag(Flag flag) { @@ -161,6 +162,8 @@ public class LocalStore extends Store implements Serializable { private ContentResolver mContentResolver; private final Account mAccount; + private final MessagePreviewCreator messagePreviewCreator; + private final AttachmentCounter attachmentCounter; /** * local://localhost/path/to/database/uuid.db @@ -178,6 +181,9 @@ public class LocalStore extends Store implements Serializable { database.setStorageProviderId(account.getLocalStorageProviderId()); uUid = account.getUuid(); + messagePreviewCreator = MessagePreviewCreator.newInstance(); + attachmentCounter = new AttachmentCounter(); + database.open(); } @@ -831,6 +837,14 @@ public class LocalStore extends Store implements Serializable { return database; } + public MessagePreviewCreator getMessagePreviewCreator() { + return messagePreviewCreator; + } + + public AttachmentCounter getAttachmentCounter() { + return attachmentCounter; + } + void notifyChange() { Uri uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + uUid + "/messages"); mContentResolver.notifyChange(uri, null); diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/MessageInfoExtractor.java b/k9mail/src/main/java/com/fsck/k9/mailstore/MessageInfoExtractor.java deleted file mode 100644 index c0e5f66dc..000000000 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/MessageInfoExtractor.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.fsck.k9.mailstore; - - -import java.util.ArrayList; -import java.util.List; - -import android.content.Context; - -import com.fsck.k9.mail.Message; -import com.fsck.k9.mail.MessagingException; -import com.fsck.k9.mail.Part; -import com.fsck.k9.mail.internet.MessageExtractor; -import com.fsck.k9.mail.internet.Viewable; - - -class MessageInfoExtractor { - private final Context context; - private final Message message; - private List viewables; - private List attachments; - - public MessageInfoExtractor(Context context, Message message) { - this.context = context; - this.message = message; - } - - public String getMessageTextPreview() throws MessagingException { - getViewablesIfNecessary(); - return MessagePreviewExtractor.extractPreview(context, viewables); - } - - public int getAttachmentCount() throws MessagingException { - getViewablesIfNecessary(); - return attachments.size(); - } - - private void getViewablesIfNecessary() throws MessagingException { - if (viewables == null) { - attachments = new ArrayList(); - viewables = MessageExtractor.getViewables(message, attachments); - } - } -} diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/MessagePreviewExtractor.java b/k9mail/src/main/java/com/fsck/k9/mailstore/MessagePreviewExtractor.java deleted file mode 100644 index bafa7c82f..000000000 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/MessagePreviewExtractor.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.fsck.k9.mailstore; - - -import java.util.List; - -import android.content.Context; -import android.text.TextUtils; - -import com.fsck.k9.R; -import com.fsck.k9.helper.HtmlConverter; -import com.fsck.k9.mail.Part; -import com.fsck.k9.mail.internet.MessageExtractor; -import com.fsck.k9.mail.internet.Viewable; -import com.fsck.k9.mail.internet.Viewable.Alternative; -import com.fsck.k9.mail.internet.Viewable.Html; -import com.fsck.k9.mail.internet.Viewable.MessageHeader; -import com.fsck.k9.mail.internet.Viewable.Textual; - - -class MessagePreviewExtractor { - private static final int MAX_PREVIEW_LENGTH = 512; - private static final int MAX_CHARACTERS_CHECKED_FOR_PREVIEW = 8192; - - public static String extractPreview(Context context, List viewables) { - StringBuilder text = new StringBuilder(); - boolean divider = false; - - for (Viewable viewable : viewables) { - if (viewable instanceof Textual) { - appendText(text, viewable, divider); - divider = true; - } else if (viewable instanceof MessageHeader) { - appendMessagePreview(context, text, (MessageHeader) viewable, divider); - divider = false; - } else if (viewable instanceof Alternative) { - appendAlternative(text, (Alternative) viewable, divider); - divider = true; - } - - if (hasMaxPreviewLengthBeenReached(text)) { - break; - } - } - - if (hasMaxPreviewLengthBeenReached(text)) { - text.setLength(MAX_PREVIEW_LENGTH - 1); - text.append('…'); - } - - return text.toString(); - } - - private static void appendText(StringBuilder text, Viewable viewable, boolean prependDivider) { - if (viewable instanceof Textual) { - appendTextual(text, (Textual) viewable, prependDivider); - } else if (viewable instanceof Alternative) { - appendAlternative(text, (Alternative) viewable, prependDivider); - } else { - throw new IllegalArgumentException("Unknown Viewable"); - } - } - - private static void appendTextual(StringBuilder text, Textual textual, boolean prependDivider) { - Part part = textual.getPart(); - - if (prependDivider) { - appendDivider(text); - } - - String textFromPart = MessageExtractor.getTextFromPart(part); - if (textFromPart == null) { - textFromPart = ""; - } else if (textual instanceof Html) { - textFromPart = HtmlConverter.htmlToText(textFromPart); - } - - text.append(stripTextForPreview(textFromPart)); - } - - private static void appendAlternative(StringBuilder text, Alternative alternative, boolean prependDivider) { - List textAlternative = alternative.getText().isEmpty() ? - alternative.getHtml() : alternative.getText(); - - boolean divider = prependDivider; - for (Viewable textViewable : textAlternative) { - appendText(text, textViewable, divider); - divider = true; - - if (hasMaxPreviewLengthBeenReached(text)) { - break; - } - } - } - - private static void appendMessagePreview(Context context, StringBuilder text, MessageHeader messageHeader, - boolean divider) { - if (divider) { - appendDivider(text); - } - - String subject = messageHeader.getMessage().getSubject(); - if (TextUtils.isEmpty(subject)) { - text.append(context.getString(R.string.preview_untitled_inner_message)); - } else { - text.append(context.getString(R.string.preview_inner_message, subject)); - } - } - - private static void appendDivider(StringBuilder text) { - text.append(" / "); - } - - private static String stripTextForPreview(String text) { - if (text == null) { - return ""; - } - - // Only look at the first 8k of a message when calculating - // the preview. This should avoid unnecessary - // memory usage on large messages - if (text.length() > MAX_CHARACTERS_CHECKED_FOR_PREVIEW) { - text = text.substring(0, MAX_CHARACTERS_CHECKED_FOR_PREVIEW); - } - - // Remove (correctly delimited by '-- \n') signatures - text = text.replaceAll("(?ms)^-- [\\r\\n]+.*", ""); - // try to remove lines of dashes in the preview - text = text.replaceAll("(?m)^----.*?$", ""); - // remove quoted text from the preview - text = text.replaceAll("(?m)^[#>].*$", ""); - // Remove a common quote header from the preview - text = text.replaceAll("(?m)^On .*wrote.?$", ""); - // Remove a more generic quote header from the preview - text = text.replaceAll("(?m)^.*\\w+:$", ""); - // Remove horizontal rules. - text = text.replaceAll("\\s*([-=_]{30,}+)\\s*", " "); - - // URLs in the preview should just be shown as "..." - They're not - // clickable and they usually overwhelm the preview - text = text.replaceAll("https?://\\S+", "..."); - // Don't show newlines in the preview - text = text.replaceAll("(\\r|\\n)+", " "); - // Collapse whitespace in the preview - text = text.replaceAll("\\s+", " "); - // Remove any whitespace at the beginning and end of the string. - text = text.trim(); - - return (text.length() <= MAX_PREVIEW_LENGTH) ? text : text.substring(0, MAX_PREVIEW_LENGTH); - } - - private static boolean hasMaxPreviewLengthBeenReached(StringBuilder text) { - return text.length() >= MAX_PREVIEW_LENGTH; - } -} diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/StoreSchemaDefinition.java b/k9mail/src/main/java/com/fsck/k9/mailstore/StoreSchemaDefinition.java index 8f93b37dd..50ce95ed4 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/StoreSchemaDefinition.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/StoreSchemaDefinition.java @@ -90,6 +90,7 @@ class StoreSchemaDefinition implements LockableDatabase.SchemaDefinition { "attachment_count INTEGER, " + "internal_date INTEGER, " + "message_id TEXT, " + + "preview_type TEXT default \"none\", " + "preview TEXT, " + "mime_type TEXT, "+ "normalized_subject_hash INTEGER, " + @@ -575,6 +576,9 @@ class StoreSchemaDefinition implements LockableDatabase.SchemaDefinition { if (db.getVersion() < 53) { removeNullValuesFromEmptyColumnInMessagesTable(db); } + if (db.getVersion() < 54) { + addPreviewTypeColumn(db); + } } db.setVersion(LocalStore.DB_VERSION); @@ -637,4 +641,9 @@ class StoreSchemaDefinition implements LockableDatabase.SchemaDefinition { private void removeNullValuesFromEmptyColumnInMessagesTable(SQLiteDatabase db) { db.execSQL("UPDATE messages SET empty = 0 WHERE empty IS NULL"); } + + private void addPreviewTypeColumn(SQLiteDatabase db) { + db.execSQL("ALTER TABLE messages ADD preview_type TEXT default \"none\""); + db.execSQL("UPDATE messages SET preview_type = 'text' WHERE preview IS NOT NULL"); + } } diff --git a/k9mail/src/main/java/com/fsck/k9/notification/NotificationContentCreator.java b/k9mail/src/main/java/com/fsck/k9/notification/NotificationContentCreator.java index b856b6fd3..85a847373 100644 --- a/k9mail/src/main/java/com/fsck/k9/notification/NotificationContentCreator.java +++ b/k9mail/src/main/java/com/fsck/k9/notification/NotificationContentCreator.java @@ -18,6 +18,7 @@ import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mailstore.LocalMessage; +import com.fsck.k9.message.preview.PreviewResult.PreviewType; class NotificationContentCreator { @@ -41,12 +42,12 @@ class NotificationContentCreator { return new NotificationContent(messageReference, displaySender, subject, preview, summary, starred); } - private CharSequence getMessagePreview(Message message) { + private CharSequence getMessagePreview(LocalMessage message) { String subject = message.getSubject(); - String snippet = message.getPreview(); + String snippet = getPreview(message); boolean isSubjectEmpty = TextUtils.isEmpty(subject); - boolean isSnippetPresent = !TextUtils.isEmpty(snippet); + boolean isSnippetPresent = message.getPreviewType() != PreviewType.NONE; if (isSubjectEmpty && isSnippetPresent) { return snippet; } @@ -65,6 +66,20 @@ class NotificationContentCreator { return preview; } + private String getPreview(LocalMessage message) { + PreviewType previewType = message.getPreviewType(); + switch (previewType) { + case NONE: + return null; + case TEXT: + return message.getPreview(); + case ENCRYPTED: + return context.getString(R.string.preview_encrypted); + } + + throw new AssertionError("Unknown preview type: " + previewType); + } + private CharSequence buildMessageSummary(String sender, String subject) { if (sender == null) { return subject; diff --git a/k9mail/src/main/java/com/fsck/k9/provider/EmailProvider.java b/k9mail/src/main/java/com/fsck/k9/provider/EmailProvider.java index d17dffcf2..5c4241433 100644 --- a/k9mail/src/main/java/com/fsck/k9/provider/EmailProvider.java +++ b/k9mail/src/main/java/com/fsck/k9/provider/EmailProvider.java @@ -143,6 +143,7 @@ public class EmailProvider extends ContentProvider { public static final String FLAGS = "flags"; public static final String ATTACHMENT_COUNT = "attachment_count"; public static final String FOLDER_ID = "folder_id"; + public static final String PREVIEW_TYPE = "preview_type"; public static final String PREVIEW = "preview"; public static final String READ = "read"; public static final String FLAGGED = "flagged"; diff --git a/k9mail/src/main/res/values/strings.xml b/k9mail/src/main/res/values/strings.xml index 01800b9f5..bcd2ed3a4 100644 --- a/k9mail/src/main/res/values/strings.xml +++ b/k9mail/src/main/res/values/strings.xml @@ -1158,5 +1158,6 @@ Please submit bug reports, contribute new features and ask questions at Incomplete message Click \'Download complete message\' to allow decryption. + *Encrypted* diff --git a/k9mail/src/test/java/com/fsck/k9/notification/NotificationContentCreatorTest.java b/k9mail/src/test/java/com/fsck/k9/notification/NotificationContentCreatorTest.java index 2688bb002..e2d8a68aa 100644 --- a/k9mail/src/test/java/com/fsck/k9/notification/NotificationContentCreatorTest.java +++ b/k9mail/src/test/java/com/fsck/k9/notification/NotificationContentCreatorTest.java @@ -9,6 +9,7 @@ import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Message.RecipientType; import com.fsck.k9.mailstore.LocalMessage; +import com.fsck.k9.message.preview.PreviewResult.PreviewType; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -76,6 +77,7 @@ public class NotificationContentCreatorTest { @Test public void createFromMessage_withoutPreview() throws Exception { + when(message.getPreviewType()).thenReturn(PreviewType.NONE); when(message.getPreview()).thenReturn(null); NotificationContent content = contentCreator.createFromMessage(account, message); @@ -84,6 +86,18 @@ public class NotificationContentCreatorTest { assertEquals(SUBJECT, content.preview.toString()); } + @Test + public void createFromMessage_withEncryptedMessage() throws Exception { + when(message.getPreviewType()).thenReturn(PreviewType.ENCRYPTED); + when(message.getPreview()).thenReturn(null); + + NotificationContent content = contentCreator.createFromMessage(account, message); + + String encrypted = "*Encrypted*"; + assertEquals(SUBJECT, content.subject); + assertEquals(SUBJECT + "\n" + encrypted, content.preview.toString()); + } + @Test public void createFromMessage_withoutSender() throws Exception { when(message.getFrom()).thenReturn(null); @@ -118,6 +132,7 @@ public class NotificationContentCreatorTest { public void createFromMessage_withoutEmptyMessage() throws Exception { when(message.getFrom()).thenReturn(null); when(message.getSubject()).thenReturn(null); + when(message.getPreviewType()).thenReturn(PreviewType.NONE); when(message.getPreview()).thenReturn(null); NotificationContent content = contentCreator.createFromMessage(account, message); @@ -145,6 +160,7 @@ public class NotificationContentCreatorTest { LocalMessage message = mock(LocalMessage.class); when(message.makeMessageReference()).thenReturn(messageReference); + when(message.getPreviewType()).thenReturn(PreviewType.TEXT); when(message.getPreview()).thenReturn(PREVIEW); when(message.getSubject()).thenReturn(SUBJECT); when(message.getFrom()).thenReturn(new Address[] { new Address(SENDER_ADDRESS, SENDER_NAME) });