Add interface for detection of encrypted messages

This includes some capabilities that are not currently used by K-9 Mail,
e.g. the ability to supply additional data to be inserted into the
database.
This commit is contained in:
cketti 2018-08-31 02:59:39 +02:00
parent 18bbd76783
commit a8f41118e3
21 changed files with 217 additions and 84 deletions

View file

@ -0,0 +1,17 @@
package com.fsck.k9.crypto
import android.content.ContentValues
import com.fsck.k9.mail.Message
import com.fsck.k9.message.extractors.PreviewResult
interface EncryptionExtractor {
fun extractEncryption(message: Message): EncryptionResult?
}
data class EncryptionResult(
val encryptionType: String,
val attachmentCount: Int,
val previewResult: PreviewResult = PreviewResult.encrypted(),
val textForSearchIndex: String? = null,
val extraContentValues: ContentValues? = null
)

View file

@ -31,6 +31,8 @@ import com.fsck.k9.DI;
import com.fsck.k9.K9;
import com.fsck.k9.controller.MessageReference;
import com.fsck.k9.backend.api.MessageRemovalListener;
import com.fsck.k9.crypto.EncryptionExtractor;
import com.fsck.k9.crypto.EncryptionResult;
import com.fsck.k9.helper.FileHelper;
import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.Address;
@ -77,6 +79,7 @@ public class LocalFolder extends Folder<LocalMessage> {
private final SearchStatusManager searchStatusManager = DI.get(SearchStatusManager.class);
private final LocalStore localStore;
private final AttachmentInfoExtractor attachmentInfoExtractor;
private final EncryptionExtractor encryptionExtractor = DI.get(EncryptionExtractor.class);
private String serverId = null;
@ -1379,17 +1382,34 @@ public class LocalFolder extends Folder<LocalMessage> {
}
try {
MessagePreviewCreator previewCreator = localStore.getMessagePreviewCreator();
PreviewResult previewResult = previewCreator.createPreview(message);
String encryptionType;
PreviewResult previewResult;
int attachmentCount;
String fulltext;
ContentValues extraContentValues;
EncryptionResult encryptionResult = encryptionExtractor.extractEncryption(message);
if (encryptionResult != null) {
encryptionType = encryptionResult.getEncryptionType();
previewResult = encryptionResult.getPreviewResult();
attachmentCount = encryptionResult.getAttachmentCount();
fulltext = encryptionResult.getTextForSearchIndex();
extraContentValues = encryptionResult.getExtraContentValues();
} else {
MessagePreviewCreator previewCreator = localStore.getMessagePreviewCreator();
MessageFulltextCreator fulltextCreator = localStore.getMessageFulltextCreator();
AttachmentCounter attachmentCounter = localStore.getAttachmentCounter();
encryptionType = null;
previewResult = previewCreator.createPreview(message);
attachmentCount = attachmentCounter.getAttachmentCount(message);
fulltext = fulltextCreator.createFulltext(message);
extraContentValues = null;
}
PreviewType previewType = previewResult.getPreviewType();
DatabasePreviewType databasePreviewType = DatabasePreviewType.fromPreviewType(previewType);
MessageFulltextCreator fulltextCreator = localStore.getMessageFulltextCreator();
String fulltext = fulltextCreator.createFulltext(message);
AttachmentCounter attachmentCounter = localStore.getAttachmentCounter();
int attachmentCount = attachmentCounter.getAttachmentCount(message);
long rootMessagePartId = saveMessageParts(db, message);
ContentValues cv = new ContentValues();
@ -1415,6 +1435,7 @@ public class LocalFolder extends Folder<LocalMessage> {
? System.currentTimeMillis() : message.getInternalDate().getTime());
cv.put("mime_type", message.getMimeType());
cv.put("empty", 0);
cv.put("encryption_type", encryptionType);
cv.put("preview_type", databasePreviewType.getDatabaseValue());
if (previewResult.isPreviewTextAvailable()) {
@ -1428,6 +1449,10 @@ public class LocalFolder extends Folder<LocalMessage> {
cv.put("message_id", messageId);
}
if (extraContentValues != null) {
cv.putAll(extraContentValues);
}
if (oldMessageId == -1) {
msgId = db.insert("messages", "uid", cv);

View file

@ -11,24 +11,12 @@ import com.fsck.k9.mail.internet.MessageExtractor;
public class AttachmentCounter {
private final EncryptionDetector encryptionDetector;
AttachmentCounter(EncryptionDetector encryptionDetector) {
this.encryptionDetector = encryptionDetector;
}
public static AttachmentCounter newInstance() {
TextPartFinder textPartFinder = new TextPartFinder();
EncryptionDetector encryptionDetector = new EncryptionDetector(textPartFinder);
return new AttachmentCounter(encryptionDetector);
return new AttachmentCounter();
}
public int getAttachmentCount(Message message) throws MessagingException {
if (encryptionDetector.isEncrypted(message)) {
return 0;
}
List<Part> attachmentParts = new ArrayList<>();
MessageExtractor.findViewablesAndAttachments(message, null, attachmentParts);

View file

@ -15,29 +15,18 @@ public class MessageFulltextCreator {
private final TextPartFinder textPartFinder;
private final EncryptionDetector encryptionDetector;
MessageFulltextCreator(TextPartFinder textPartFinder, EncryptionDetector encryptionDetector) {
MessageFulltextCreator(TextPartFinder textPartFinder) {
this.textPartFinder = textPartFinder;
this.encryptionDetector = encryptionDetector;
}
public static MessageFulltextCreator newInstance() {
TextPartFinder textPartFinder = new TextPartFinder();
EncryptionDetector encryptionDetector = new EncryptionDetector(textPartFinder);
return new MessageFulltextCreator(textPartFinder, encryptionDetector);
return new MessageFulltextCreator(textPartFinder);
}
public String createFulltext(@NonNull Message message) {
if (encryptionDetector.isEncrypted(message)) {
return null;
}
return extractText(message);
}
private String extractText(Message message) {
Part textPart = textPartFinder.findFirstTextPart(message);
if (textPart == null || hasEmptyBody(textPart)) {
return null;

View file

@ -10,32 +10,20 @@ 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) {
MessagePreviewCreator(TextPartFinder textPartFinder, PreviewTextExtractor previewTextExtractor) {
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);
return new MessagePreviewCreator(textPartFinder, previewTextExtractor);
}
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();

View file

@ -12,7 +12,7 @@ import com.fsck.k9.mail.Part;
import static com.fsck.k9.mail.internet.MimeUtility.isSameMimeType;
class TextPartFinder {
public class TextPartFinder {
@Nullable
public Part findFirstTextPart(@NonNull Part part) {
String mimeType = part.getMimeType();

View file

@ -1,6 +1,7 @@
package com.fsck.k9
import android.app.Application
import com.fsck.k9.crypto.EncryptionExtractor
import com.fsck.k9.storage.storageModule
import com.nhaarman.mockito_kotlin.mock
import org.koin.dsl.module.applicationContext
@ -20,4 +21,5 @@ class TestApp : Application() {
val testModule = applicationContext {
bean { AppConfig(emptyList()) }
bean { mock<CoreResourceProvider>() }
bean { mock<EncryptionExtractor>() }
}

View file

@ -21,35 +21,19 @@ 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);
previewCreator = new MessagePreviewCreator(textPartFinder, previewTextExtractor);
}
@Test
public void createPreview_withEncryptedMessage() throws Exception {
public void createPreview_withoutTextPart() {
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);
@ -63,7 +47,6 @@ public class MessagePreviewCreatorTest {
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);
@ -77,7 +60,6 @@ public class MessagePreviewCreatorTest {
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");
@ -92,7 +74,6 @@ public class MessagePreviewCreatorTest {
public void createPreview_withPreviewTextExtractorThrowing() 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)).thenThrow(new PreviewExtractionException(""));

View file

@ -0,0 +1,33 @@
apply plugin: 'com.android.library'
apply plugin: 'org.jetbrains.kotlin.android'
apply from: "${rootProject.projectDir}/gradle/plugins/checkstyle-android.gradle"
apply from: "${rootProject.projectDir}/gradle/plugins/findbugs-android.gradle"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${versions.kotlin}"
implementation project(":app:core")
testImplementation "junit:junit:${versions.junit}"
testImplementation "org.mockito:mockito-core:${versions.mockito}"
}
android {
compileSdkVersion buildConfig.compileSdk
buildToolsVersion buildConfig.buildTools
defaultConfig {
minSdkVersion buildConfig.minSdk
}
lintOptions {
abortOnError false
lintConfig file("$rootProject.projectDir/config/lint/lint.xml")
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
}

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.fsck.k9.crypto.openpgp" />

View file

@ -1,4 +1,4 @@
package com.fsck.k9.message.extractors;
package com.fsck.k9.crypto.openpgp;
import android.support.annotation.NonNull;
@ -9,10 +9,12 @@ 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.message.extractors.TextPartFinder;
import static com.fsck.k9.mail.internet.MimeUtility.isSameMimeType;
//FIXME: Make this only detect OpenPGP messages. Move support for S/MIME messages to separate module.
class EncryptionDetector {
private final TextPartFinder textPartFinder;

View file

@ -0,0 +1,31 @@
package com.fsck.k9.crypto.openpgp
import com.fsck.k9.crypto.EncryptionExtractor
import com.fsck.k9.crypto.EncryptionResult
import com.fsck.k9.mail.Message
import com.fsck.k9.message.extractors.TextPartFinder
class OpenPgpEncryptionExtractor internal constructor(
private val encryptionDetector: EncryptionDetector
) : EncryptionExtractor {
override fun extractEncryption(message: Message): EncryptionResult? {
return if (encryptionDetector.isEncrypted(message)) {
EncryptionResult(ENCRYPTION_TYPE, 0)
} else {
null
}
}
companion object {
const val ENCRYPTION_TYPE = "openpgp"
@JvmStatic
fun newInstance(): OpenPgpEncryptionExtractor {
val textPartFinder = TextPartFinder()
val encryptionDetector = EncryptionDetector(textPartFinder)
return OpenPgpEncryptionExtractor(encryptionDetector)
}
}
}

View file

@ -1,14 +1,15 @@
package com.fsck.k9.message.extractors;
package com.fsck.k9.crypto.openpgp;
import com.fsck.k9.mail.Message;
import com.fsck.k9.message.extractors.TextPartFinder;
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 com.fsck.k9.crypto.openpgp.MessageCreationHelper.createMessage;
import static com.fsck.k9.crypto.openpgp.MessageCreationHelper.createMultipartMessage;
import static com.fsck.k9.crypto.openpgp.MessageCreationHelper.createPart;
import static com.fsck.k9.crypto.openpgp.MessageCreationHelper.createTextMessage;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
@ -24,14 +25,14 @@ public class EncryptionDetectorTest {
@Before
public void setUp() throws Exception {
public void setUp() {
textPartFinder = mock(TextPartFinder.class);
encryptionDetector = new EncryptionDetector(textPartFinder);
}
@Test
public void isEncrypted_withTextPlain_shouldReturnFalse() throws Exception {
public void isEncrypted_withTextPlain_shouldReturnFalse() {
Message message = createTextMessage("text/plain", "plain text");
boolean encrypted = encryptionDetector.isEncrypted(message);
@ -50,7 +51,7 @@ public class EncryptionDetectorTest {
}
@Test
public void isEncrypted_withSMimePart_shouldReturnTrue() throws Exception {
public void isEncrypted_withSMimePart_shouldReturnTrue() {
Message message = createMessage("application/pkcs7-mime");
boolean encrypted = encryptionDetector.isEncrypted(message);
@ -69,7 +70,7 @@ public class EncryptionDetectorTest {
}
@Test
public void isEncrypted_withInlinePgp_shouldReturnTrue() throws Exception {
public void isEncrypted_withInlinePgp_shouldReturnTrue() {
Message message = createTextMessage("text/plain", "" +
"-----BEGIN PGP MESSAGE-----" + CRLF +
"some encrypted stuff here" + CRLF +
@ -82,7 +83,7 @@ public class EncryptionDetectorTest {
}
@Test
public void isEncrypted_withPlainTextAndPreambleWithInlinePgp_shouldReturnFalse() throws Exception {
public void isEncrypted_withPlainTextAndPreambleWithInlinePgp_shouldReturnFalse() {
Message message = createTextMessage("text/plain", "" +
"preamble" + CRLF +
"-----BEGIN PGP MESSAGE-----" + CRLF +
@ -97,7 +98,7 @@ public class EncryptionDetectorTest {
}
@Test
public void isEncrypted_withQuotedInlinePgp_shouldReturnFalse() throws Exception {
public void isEncrypted_withQuotedInlinePgp_shouldReturnFalse() {
Message message = createTextMessage("text/plain", "" +
"good talk!" + CRLF +
CRLF +

View file

@ -0,0 +1,51 @@
package com.fsck.k9.crypto.openpgp;
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 createPart(String mimeType) throws MessagingException {
BinaryMemoryBody body = new BinaryMemoryBody(new byte[0], "utf-8");
return new MimeBodyPart(body, mimeType);
}
public static Message createTextMessage(String mimeType, String text) {
TextBody body = new TextBody(text);
return createMessage(mimeType, body);
}
public static Message createMultipartMessage(String mimeType, BodyPart... parts) {
MimeMultipart body = createMultipartBody(mimeType, parts);
return createMessage(mimeType, body);
}
public static Message createMessage(String mimeType) {
return createMessage(mimeType, null);
}
private static Message createMessage(String mimeType, Body body) {
MimeMessage message = new MimeMessage();
message.setBody(body);
message.setHeader(MimeHeader.HEADER_CONTENT_TYPE, mimeType);
return message;
}
private static MimeMultipart createMultipartBody(String mimeType, BodyPart[] parts) {
MimeMultipart multipart = new MimeMultipart(mimeType, "boundary");
for (BodyPart part : parts) {
multipart.addBodyPart(part);
}
return multipart;
}
}

View file

@ -15,6 +15,7 @@ dependencies {
implementation project(":app:ui")
implementation project(":app:core")
implementation project(":app:storage")
implementation project(":app:crypto-openpgp")
implementation project(":backend:imap")
implementation project(":backend:pop3")
implementation project(":backend:webdav")

View file

@ -2,6 +2,8 @@ package com.fsck.k9
import com.fsck.k9.backends.backendsModule
import com.fsck.k9.controller.ControllerExtension
import com.fsck.k9.crypto.EncryptionExtractor
import com.fsck.k9.crypto.openpgp.OpenPgpEncryptionExtractor
import com.fsck.k9.external.BroadcastSenderListener
import com.fsck.k9.external.externalModule
import com.fsck.k9.notification.notificationModule
@ -23,6 +25,7 @@ private val mainAppModule = applicationContext {
))
}
bean("controllerExtensions") { emptyList<ControllerExtension>() }
bean { OpenPgpEncryptionExtractor.newInstance() as EncryptionExtractor }
}
val appModules = listOf(

View file

@ -12,7 +12,7 @@ import timber.log.Timber;
class StoreSchemaDefinition implements SchemaDefinition {
static final int DB_VERSION = 65;
static final int DB_VERSION = 66;
private final MigrationsHelper migrationsHelper;
@ -134,7 +134,8 @@ class StoreSchemaDefinition implements SchemaDefinition {
"flagged INTEGER default 0, " +
"answered INTEGER default 0, " +
"forwarded INTEGER default 0, " +
"message_part_id INTEGER" +
"message_part_id INTEGER," +
"encryption_type TEXT" +
")");
db.execSQL("DROP TABLE IF EXISTS message_parts");

View file

@ -0,0 +1,13 @@
package com.fsck.k9.storage.migrations
import android.database.sqlite.SQLiteDatabase
internal object MigrationTo66 {
@JvmStatic
fun addEncryptionTypeColumnToMessagesTable(db: SQLiteDatabase) {
db.execSQL("ALTER TABLE messages ADD encryption_type TEXT")
db.execSQL("UPDATE messages SET encryption_type = 'openpgp' WHERE preview_type = 'encrypted'")
}
}

View file

@ -88,6 +88,8 @@ public class Migrations {
MigrationTo64.addExtraValuesTables(db);
case 64:
MigrationTo65.addLocalOnlyColumnToFoldersTable(db, migrationsHelper);
case 65:
MigrationTo66.addEncryptionTypeColumnToMessagesTable(db);
}
if (shouldBuildFtsTable) {

View file

@ -6,6 +6,7 @@ import com.fsck.k9.Core
import com.fsck.k9.CoreResourceProvider
import com.fsck.k9.DI
import com.fsck.k9.K9
import com.fsck.k9.crypto.EncryptionExtractor
import com.nhaarman.mockito_kotlin.mock
import org.koin.dsl.module.applicationContext
@ -24,4 +25,5 @@ class TestApp : Application() {
val testModule = applicationContext {
bean { AppConfig(emptyList()) }
bean { mock<CoreResourceProvider>() }
bean { mock<EncryptionExtractor>() }
}

View file

@ -2,6 +2,7 @@ include ':app:k9mail'
include ':app:ui'
include ':app:core'
include ':app:storage'
include ':app:crypto-openpgp'
include ':mail:common'
include ':mail:testing'
include ':mail:protocols:imap'