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