Merge pull request #2967 from lazycodeninja/master
Don't crash when replacing content URIs Fixes #1988
This commit is contained in:
commit
a95e897803
3 changed files with 245 additions and 6 deletions
|
@ -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();
|
||||
|
|
6
k9mail/src/test/java/com/fsck/k9/KotlinHelper.kt
Normal file
6
k9mail/src/test/java/com/fsck/k9/KotlinHelper.kt
Normal 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)
|
|
@ -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))
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue