diff --git a/k9mail/src/main/java/com/fsck/k9/mailstore/migrations/MigrationTo51.java b/k9mail/src/main/java/com/fsck/k9/mailstore/migrations/MigrationTo51.java index 65f0769ab..fa950158b 100644 --- a/k9mail/src/main/java/com/fsck/k9/mailstore/migrations/MigrationTo51.java +++ b/k9mail/src/main/java/com/fsck/k9/mailstore/migrations/MigrationTo51.java @@ -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(); diff --git a/k9mail/src/test/java/com/fsck/k9/KotlinHelper.kt b/k9mail/src/test/java/com/fsck/k9/KotlinHelper.kt new file mode 100644 index 000000000..d42f0e24c --- /dev/null +++ b/k9mail/src/test/java/com/fsck/k9/KotlinHelper.kt @@ -0,0 +1,6 @@ +package com.fsck.k9 + +import org.mockito.Mockito +import org.mockito.stubbing.OngoingStubbing + +fun whenever(methodCall: T): OngoingStubbing = Mockito.`when`(methodCall) diff --git a/k9mail/src/test/java/com/fsck/k9/mailstore/migrations/MigrationTo51Test.kt b/k9mail/src/test/java/com/fsck/k9/mailstore/migrations/MigrationTo51Test.kt new file mode 100644 index 000000000..934b40081 --- /dev/null +++ b/k9mail/src/test/java/com/fsck/k9/mailstore/migrations/MigrationTo51Test.kt @@ -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("""""", htmlMessagePartBody()) + } + + @Test + fun db51MigrateMessageFormat_withMultipartMixedMessageWithAttachmentWithUnusualContentID_updatesContentReference() { + addMultipartMixedMessageWithAttachment(attachmentContentId = "a\$b@host") + + MigrationTo51.db51MigrateMessageFormat(database, mockMigrationsHelper) + + assertEquals("", 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 = "") + } + + private fun addMultipartAlternativeMessage() { + insertMessage(mimeType = "multipart/alternative", htmlContent = "") + } + + private fun addMultipartMixedMessage() { + insertMessage(mimeType = "multipart/mixed", htmlContent = "", textContent = "Text") + } + + private fun addMultipartMixedMessageWithAttachment(attachmentContentId: String) { + insertMessage( + mimeType = "multipart/mixed", + htmlContent = """""", + 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)) + ) + ) + } +}