Skip to content

Commit

Permalink
Add interface for detection of encrypted messages
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
cketti committed Sep 2, 2018
1 parent 18bbd76 commit a8f4111
Show file tree
Hide file tree
Showing 21 changed files with 217 additions and 84 deletions.
17 changes: 17 additions & 0 deletions app/core/src/main/java/com/fsck/k9/crypto/EncryptionExtractor.kt
@@ -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
)
41 changes: 33 additions & 8 deletions app/core/src/main/java/com/fsck/k9/mailstore/LocalFolder.java
Expand Up @@ -31,6 +31,8 @@
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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1379,17 +1382,34 @@ private void saveMessage(SQLiteDatabase db, Message message, boolean copy, Map<S
}

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();
Expand All @@ -1415,6 +1435,7 @@ private void saveMessage(SQLiteDatabase db, Message message, boolean copy, Map<S
? 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()) {
Expand All @@ -1428,6 +1449,10 @@ private void saveMessage(SQLiteDatabase db, Message message, boolean copy, Map<S
cv.put("message_id", messageId);
}

if (extraContentValues != null) {
cv.putAll(extraContentValues);
}

if (oldMessageId == -1) {
msgId = db.insert("messages", "uid", cv);

Expand Down
Expand Up @@ -11,24 +11,12 @@


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);

Expand Down
Expand Up @@ -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;
Expand Down
Expand Up @@ -10,32 +10,20 @@
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();
Expand Down
Expand Up @@ -12,7 +12,7 @@
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();
Expand Down
2 changes: 2 additions & 0 deletions app/core/src/test/java/com/fsck/k9/TestApp.kt
@@ -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
Expand All @@ -20,4 +21,5 @@ class TestApp : Application() {
val testModule = applicationContext {
bean { AppConfig(emptyList()) }
bean { mock<CoreResourceProvider>() }
bean { mock<EncryptionExtractor>() }
}
Expand Up @@ -21,35 +21,19 @@
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);
Expand All @@ -63,7 +47,6 @@ public void createPreview_withoutTextPart() throws Exception {
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);
Expand All @@ -77,7 +60,6 @@ public void createPreview_withEmptyTextPart() throws Exception {
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");

Expand All @@ -92,7 +74,6 @@ public void createPreview_withTextPart() throws Exception {
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(""));

Expand Down
33 changes: 33 additions & 0 deletions app/crypto-openpgp/build.gradle
@@ -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
}
}
2 changes: 2 additions & 0 deletions app/crypto-openpgp/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.fsck.k9.crypto.openpgp" />
@@ -1,4 +1,4 @@
package com.fsck.k9.message.extractors;
package com.fsck.k9.crypto.openpgp;


import android.support.annotation.NonNull;
Expand All @@ -9,10 +9,12 @@
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;

Expand Down
@@ -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)
}
}
}

0 comments on commit a8f4111

Please sign in to comment.