Merge pull request #2967 from lazycodeninja/master

Don't crash when replacing content URIs

Fixes #1988
This commit is contained in:
cketti 2017-12-17 01:44:14 +01:00 committed by GitHub
commit a95e897803
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 245 additions and 6 deletions

View file

@ -7,7 +7,6 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern;
import android.content.ContentValues;
import android.database.Cursor;
@ -17,10 +16,8 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import timber.log.Timber;
import com.fsck.k9.Account;
import com.fsck.k9.K9;
import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.internet.MimeHeader;
@ -28,6 +25,7 @@ import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mailstore.StorageManager;
import org.apache.james.mime4j.codec.QuotedPrintableOutputStream;
import org.apache.james.mime4j.util.MimeUtil;
import timber.log.Timber;
class MigrationTo51 {
@ -65,7 +63,7 @@ class MigrationTo51 {
File attachmentDirNew, attachmentDirOld;
Account account = migrationsHelper.getAccount();
attachmentDirNew = StorageManager.getInstance(K9.app).getAttachmentDirectory(
attachmentDirNew = StorageManager.getInstance(migrationsHelper.getContext()).getAttachmentDirectory(
account.getUuid(), account.getLocalStorageProviderId());
attachmentDirOld = renameOldAttachmentDirAndCreateNew(account, attachmentDirNew);
@ -341,7 +339,7 @@ class MigrationTo51 {
private static MimeStructureState migrateComplexMailContent(SQLiteDatabase db,
File attachmentDirOld, File attachmentDirNew, long messageId, String htmlContent, String textContent,
MimeHeader mimeHeader, MimeStructureState structureState) throws IOException {
Timber.d("Processing mail with complex data structure as multipart/mixed");
Timber.d("Processing mail with complex data structure as multipart/mixed - message ID %d", messageId);
String boundary = MimeUtility.getHeaderParameter(
mimeHeader.getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE), "boundary");
@ -389,8 +387,9 @@ class MigrationTo51 {
while (cursor.moveToNext()) {
String contentUriString = cursor.getString(0);
String contentId = cursor.getString(1);
// this is not super efficient, but occurs only once or twice
htmlContent = htmlContent.replaceAll(Pattern.quote(contentUriString), "cid:" + contentId);
htmlContent = htmlContent.replace(contentUriString, "cid:" + contentId);
}
} finally {
cursor.close();

View file

@ -0,0 +1,6 @@
package com.fsck.k9
import org.mockito.Mockito
import org.mockito.stubbing.OngoingStubbing
fun <T> whenever(methodCall: T): OngoingStubbing<T> = Mockito.`when`(methodCall)

View file

@ -0,0 +1,234 @@
package com.fsck.k9.mailstore.migrations
import android.database.sqlite.SQLiteDatabase
import com.fsck.k9.Account
import com.fsck.k9.mailstore.StorageManager
import com.fsck.k9.whenever
import org.apache.commons.io.IOUtils
import org.apache.james.mime4j.codec.QuotedPrintableInputStream
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
import java.io.ByteArrayInputStream
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE)
class MigrationTo51Test {
private lateinit var mockMigrationsHelper: MigrationsHelper
private lateinit var database: SQLiteDatabase
@Before
fun setUp() {
val storageManager = StorageManager.getInstance(RuntimeEnvironment.application)
storageManager.defaultProviderId
val account = mock(Account::class.java)
whenever(account.uuid).thenReturn("001")
whenever(account.localStorageProviderId).thenReturn(storageManager.defaultProviderId)
mockMigrationsHelper = mock(MigrationsHelper::class.java)
whenever(mockMigrationsHelper.context).thenReturn(RuntimeEnvironment.application)
whenever(mockMigrationsHelper.account).thenReturn(account)
database = createWithV50Table()
}
@Test
fun db51MigrateMessageFormat_canMigrateEmptyMessagesTable() {
MigrationTo51.db51MigrateMessageFormat(database, mockMigrationsHelper)
}
@Test
fun db51MigrateMessageFormat_canMigrateTextPlainMessage() {
addTextPlainMessage()
MigrationTo51.db51MigrateMessageFormat(database, mockMigrationsHelper)
}
@Test
fun db51MigrateMessageFormat_canMigrateTextHtmlMessage() {
addTextHtmlMessage()
MigrationTo51.db51MigrateMessageFormat(database, mockMigrationsHelper)
}
@Test
fun db51MigrateMessageFormat_canMigrateMultipartAlternativeMessage() {
addMultipartAlternativeMessage()
MigrationTo51.db51MigrateMessageFormat(database, mockMigrationsHelper)
}
@Test
fun db51MigrateMessageFormat_canMigrateMultipartMixedMessage() {
addMultipartMixedMessage()
MigrationTo51.db51MigrateMessageFormat(database, mockMigrationsHelper)
}
@Test
fun db51MigrateMessageFormat_canMigrateMultipartMixedMessageWithAttachment() {
addMultipartMixedMessageWithAttachment(attachmentContentId = "content*user@host")
MigrationTo51.db51MigrateMessageFormat(database, mockMigrationsHelper)
}
@Test
fun db51MigrateMessageFormat_withMultipartMixedMessageWithAttachment_storesMessagePart() {
addMultipartMixedMessageWithAttachment(attachmentContentId = "content*user@host")
MigrationTo51.db51MigrateMessageFormat(database, mockMigrationsHelper)
val isNotEmpty = loadHtmlMessagePartCursor().moveToNext()
assertTrue(isNotEmpty)
}
@Test
fun db51MigrateMessageFormat_withMultipartMixedMessageWithAttachment_updatesContentReference() {
addMultipartMixedMessageWithAttachment(attachmentContentId = "content*user@host")
MigrationTo51.db51MigrateMessageFormat(database, mockMigrationsHelper)
assertEquals("""<html><img src="cid:content*user@host" /></html>""", htmlMessagePartBody())
}
@Test
fun db51MigrateMessageFormat_withMultipartMixedMessageWithAttachmentWithUnusualContentID_updatesContentReference() {
addMultipartMixedMessageWithAttachment(attachmentContentId = "a\$b@host")
MigrationTo51.db51MigrateMessageFormat(database, mockMigrationsHelper)
assertEquals("<html><img src=\"cid:a\$b@host\" /></html>", htmlMessagePartBody())
}
private fun createWithV50Table(): SQLiteDatabase {
val database = SQLiteDatabase.create(null)
database.execSQL("""
CREATE TABLE messages (
id INTEGER PRIMARY KEY,
deleted INTEGER default 0,
folder_id INTEGER, uid TEXT,
subject TEXT,
date INTEGER,
sender_list TEXT,
to_list TEXT,
cc_list TEXT,
bcc_list TEXT,
reply_to_list TEXT,
attachment_count INTEGER,
internal_date INTEGER,
message_id TEXT,
preview TEXT,
mime_type TEXT,
html_content TEXT,
text_content TEXT,
flags TEXT,
normalized_subject_hash INTEGER,
empty INTEGER default 0,
read INTEGER default 0,
flagged INTEGER default 0,
answered INTEGER default 0
)
""".trimIndent()
)
database.execSQL("""
CREATE TABLE headers (
id INTEGER PRIMARY KEY,
name TEXT,
value TEXT,
message_id INTEGER
)
""".trimIndent()
)
database.execSQL("""
CREATE TABLE attachments (
id INTEGER PRIMARY KEY,
size INTEGER,
name TEXT,
mime_type TEXT,
store_data TEXT,
content_uri TEXT,
content_id TEXT,
content_disposition TEXT,
message_id INTEGER
)
""".trimIndent()
)
return database
}
private fun addTextPlainMessage() {
insertMessage(mimeType = "text/plain", textContent = "Text")
}
private fun addTextHtmlMessage() {
insertMessage(mimeType = "text/html", htmlContent = "<html></html>")
}
private fun addMultipartAlternativeMessage() {
insertMessage(mimeType = "multipart/alternative", htmlContent = "<html></html>")
}
private fun addMultipartMixedMessage() {
insertMessage(mimeType = "multipart/mixed", htmlContent = "<html></html>", textContent = "Text")
}
private fun addMultipartMixedMessageWithAttachment(attachmentContentId: String) {
insertMessage(
mimeType = "multipart/mixed",
htmlContent = """<html><img src="testUri" /></html>""",
attachmentCount = 1
)
insertImageAttachment(attachmentContentId)
}
private fun insertMessage(
mimeType: String,
htmlContent: String? = null,
textContent: String? = null,
attachmentCount: Int = 0
) {
database.execSQL(
"INSERT INTO messages (flags, html_content, text_content, mime_type, attachment_count) " +
"VALUES (?, ?, ?, ?, ?)",
arrayOf("", htmlContent, textContent, mimeType, attachmentCount)
)
}
private fun insertImageAttachment(cid: String) {
database.execSQL(
"""
INSERT INTO attachments
(size, name, mime_type, store_data, content_uri, content_id, content_disposition, message_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""".trimIndent(),
arrayOf(1, "a.jpg", "image/jpeg", "a", "testUri", cid, "disposition", 1)
)
}
private fun loadHtmlMessagePartCursor() =
database.query("message_parts", arrayOf("data"), "mime_type = 'text/html'", null, null, null, null)
private fun htmlMessagePartBody(): String {
val cursor = loadHtmlMessagePartCursor()
if (!cursor.moveToNext()) {
throw AssertionError("No message part found")
}
return IOUtils.toString(
QuotedPrintableInputStream(
ByteArrayInputStream(cursor.getBlob(0))
)
)
}
}