Re-implement copy message functionality in MessageStore

Instead of loading a message into memory and then saving it to the new folder the new code copies the database entries and data files.
This commit is contained in:
cketti 2021-04-21 04:38:45 +02:00
parent 9ef105d74f
commit e05bc461b5
7 changed files with 681 additions and 7 deletions

View file

@ -27,6 +27,26 @@ interface MessageStore {
*/
fun saveLocalMessage(folderId: Long, messageData: SaveMessageData, existingMessageId: Long? = null): Long
/**
* Copy a message to another folder.
*
* @return The message's database ID in the destination folder.
*/
fun copyMessage(messageId: Long, destinationFolderId: Long): Long
/**
* Copy messages to another folder.
*
* @return A mapping of the original message database ID to the new message database ID.
*/
fun copyMessages(messageIds: Collection<Long>, destinationFolderId: Long): Map<Long, Long> {
return messageIds
.map { messageId ->
messageId to copyMessage(messageId, destinationFolderId)
}
.toMap()
}
/**
* Move a message to another folder.
*

View file

@ -7,7 +7,7 @@ import com.fsck.k9.mailstore.StorageManager.InternalStorageProvider
import java.io.File
import timber.log.Timber
class AttachmentFileManager(
internal class AttachmentFileManager(
private val storageManager: StorageManager,
private val accountUuid: String
) {
@ -23,7 +23,13 @@ class AttachmentFileManager(
FileHelper.renameOrMoveByCopying(temporaryFile, destinationFile)
}
private fun getAttachmentFile(messagePartId: Long): File {
fun copyFile(sourceMessagePartId: Long, destinationMessagePartId: Long) {
val sourceFile = getAttachmentFile(sourceMessagePartId)
val destinationFile = getAttachmentFile(destinationMessagePartId)
sourceFile.copyTo(destinationFile)
}
fun getAttachmentFile(messagePartId: Long): File {
val attachmentDirectory = storageManager.getAttachmentDirectory(accountUuid, InternalStorageProvider.ID)
return File(attachmentDirectory, messagePartId.toString())
}

View file

@ -0,0 +1,315 @@
package com.fsck.k9.storage.messages
import android.content.ContentValues
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import androidx.core.database.getBlobOrNull
import androidx.core.database.getLongOrNull
import androidx.core.database.getStringOrNull
import com.fsck.k9.K9
import com.fsck.k9.mailstore.LockableDatabase
import java.util.UUID
internal class CopyMessageOperations(
private val lockableDatabase: LockableDatabase,
private val attachmentFileManager: AttachmentFileManager,
private val threadMessageOperations: ThreadMessageOperations
) {
fun copyMessage(messageId: Long, destinationFolderId: Long): Long {
return lockableDatabase.execute(true) { database ->
val newMessageId = copyMessage(database, messageId, destinationFolderId)
copyFulltextEntry(database, newMessageId, messageId)
newMessageId
}
}
private fun copyMessage(
database: SQLiteDatabase,
messageId: Long,
destinationFolderId: Long
): Long {
val rootMessagePart = copyMessageParts(database, messageId)
val threadInfo = threadMessageOperations.doMessageThreading(
database,
folderId = destinationFolderId,
threadHeaders = threadMessageOperations.getMessageThreadHeaders(database, messageId)
)
return if (threadInfo?.messageId != null) {
updateMessageRow(
database,
sourceMessageId = messageId,
destinationMessageId = threadInfo.messageId,
destinationFolderId,
rootMessagePart
)
} else {
val newMessageId = insertMessageRow(
database,
sourceMessageId = messageId,
destinationFolderId = destinationFolderId,
rootMessagePartId = rootMessagePart
)
if (threadInfo?.threadId == null) {
threadMessageOperations.createThreadEntry(
database,
newMessageId,
threadInfo?.rootId,
threadInfo?.parentId
)
}
newMessageId
}
}
private fun copyMessageParts(database: SQLiteDatabase, messageId: Long): Long {
return database.rawQuery(
"""
SELECT
message_parts.id,
message_parts.type,
message_parts.root,
message_parts.parent,
message_parts.seq,
message_parts.mime_type,
message_parts.decoded_body_size,
message_parts.display_name,
message_parts.header,
message_parts.encoding,
message_parts.charset,
message_parts.data_location,
message_parts.data,
message_parts.preamble,
message_parts.epilogue,
message_parts.boundary,
message_parts.content_id,
message_parts.server_extra
FROM messages
JOIN message_parts ON (message_parts.root = messages.message_part_id)
WHERE messages.id = ?
ORDER BY message_parts.seq
""".trimIndent(),
arrayOf(messageId.toString())
).use { cursor ->
if (!cursor.moveToNext()) error("No message part found for message with ID $messageId")
val rootMessagePart = cursor.readMessagePart()
val rootMessagePartId = writeMessagePart(
database = database,
databaseMessagePart = rootMessagePart,
newRootId = null,
newParentId = -1
)
val messagePartIdMapping = mutableMapOf<Long, Long>()
messagePartIdMapping[rootMessagePart.id] = rootMessagePartId
while (cursor.moveToNext()) {
val messagePart = cursor.readMessagePart()
messagePartIdMapping[messagePart.id] = writeMessagePart(
database = database,
databaseMessagePart = messagePart,
newRootId = rootMessagePartId,
newParentId = messagePartIdMapping[messagePart.parent] ?: error("parent ID not found")
)
}
rootMessagePartId
}
}
private fun writeMessagePart(
database: SQLiteDatabase,
databaseMessagePart: DatabaseMessagePart,
newRootId: Long?,
newParentId: Long
): Long {
val values = ContentValues().apply {
put("type", databaseMessagePart.type)
put("root", newRootId)
put("parent", newParentId)
put("seq", databaseMessagePart.seq)
put("mime_type", databaseMessagePart.mimeType)
put("decoded_body_size", databaseMessagePart.decodedBodySize)
put("display_name", databaseMessagePart.displayName)
put("header", databaseMessagePart.header)
put("encoding", databaseMessagePart.encoding)
put("charset", databaseMessagePart.charset)
put("data_location", databaseMessagePart.dataLocation)
put("data", databaseMessagePart.data)
put("preamble", databaseMessagePart.preamble)
put("epilogue", databaseMessagePart.epilogue)
put("boundary", databaseMessagePart.boundary)
put("content_id", databaseMessagePart.contentId)
put("server_extra", databaseMessagePart.serverExtra)
}
val messagePartId = database.insert("message_parts", null, values)
if (databaseMessagePart.dataLocation == DataLocation.ON_DISK) {
attachmentFileManager.copyFile(databaseMessagePart.id, messagePartId)
}
return messagePartId
}
private fun updateMessageRow(
database: SQLiteDatabase,
sourceMessageId: Long,
destinationMessageId: Long,
destinationFolderId: Long,
rootMessagePartId: Long
): Long {
val values = readMessageToContentValues(database, sourceMessageId, destinationFolderId, rootMessagePartId)
database.update("messages", values, "id = ?", arrayOf(destinationMessageId.toString()))
return destinationMessageId
}
private fun insertMessageRow(
database: SQLiteDatabase,
sourceMessageId: Long,
destinationFolderId: Long,
rootMessagePartId: Long
): Long {
val values = readMessageToContentValues(database, sourceMessageId, destinationFolderId, rootMessagePartId)
return database.insert("messages", null, values)
}
private fun readMessageToContentValues(
database: SQLiteDatabase,
sourceMessageId: Long,
destinationFolderId: Long,
rootMessagePartId: Long
): ContentValues {
val values = readMessageToContentValues(database, sourceMessageId)
return values.apply {
put("folder_id", destinationFolderId)
put("uid", K9.LOCAL_UID_PREFIX + UUID.randomUUID().toString())
put("message_part_id", rootMessagePartId)
}
}
private fun copyFulltextEntry(database: SQLiteDatabase, newMessageId: Long, messageId: Long) {
database.execSQL(
"""
INSERT OR REPLACE INTO messages_fulltext (docid, fulltext)
SELECT ?, fulltext FROM messages_fulltext WHERE docid = ?
""".trimIndent(),
arrayOf(newMessageId.toString(), messageId.toString())
)
}
private fun readMessageToContentValues(database: SQLiteDatabase, messageId: Long): ContentValues {
return database.query(
"messages",
arrayOf(
"deleted",
"subject",
"date",
"flags",
"sender_list",
"to_list",
"cc_list",
"bcc_list",
"reply_to_list",
"attachment_count",
"internal_date",
"message_id",
"preview_type",
"preview",
"mime_type",
"normalized_subject_hash",
"empty",
"read",
"flagged",
"answered",
"forwarded",
"encryption_type"
),
"id = ?",
arrayOf(messageId.toString()),
null, null, null
).use { cursor ->
if (!cursor.moveToNext()) error("Message with ID $messageId not found")
ContentValues().apply {
put("deleted", cursor.getInt(0))
put("subject", cursor.getStringOrNull(1))
put("date", cursor.getLong(2))
put("flags", cursor.getStringOrNull(3))
put("sender_list", cursor.getStringOrNull(4))
put("to_list", cursor.getStringOrNull(5))
put("cc_list", cursor.getStringOrNull(6))
put("bcc_list", cursor.getStringOrNull(7))
put("reply_to_list", cursor.getStringOrNull(8))
put("attachment_count", cursor.getInt(9))
put("internal_date", cursor.getLong(10))
put("message_id", cursor.getStringOrNull(11))
put("preview_type", cursor.getStringOrNull(12))
put("preview", cursor.getStringOrNull(13))
put("mime_type", cursor.getStringOrNull(14))
put("normalized_subject_hash", cursor.getLong(15))
put("empty", cursor.getInt(16))
put("read", cursor.getInt(17))
put("flagged", cursor.getInt(18))
put("answered", cursor.getInt(19))
put("forwarded", cursor.getInt(20))
put("encryption_type", cursor.getStringOrNull(21))
}
}
}
private fun Cursor.readMessagePart(): DatabaseMessagePart {
return DatabaseMessagePart(
id = getLong(0),
type = getInt(1),
root = getLong(2),
parent = getLong(3),
seq = getInt(4),
mimeType = getString(5),
decodedBodySize = getLongOrNull(6),
displayName = getStringOrNull(7),
header = getBlobOrNull(8),
encoding = getStringOrNull(9),
charset = getStringOrNull(10),
dataLocation = getInt(11),
data = getBlobOrNull(12),
preamble = getBlobOrNull(13),
epilogue = getBlobOrNull(14),
boundary = getStringOrNull(15),
contentId = getStringOrNull(16),
serverExtra = getStringOrNull(17)
)
}
}
private class DatabaseMessagePart(
val id: Long,
val type: Int,
val root: Long,
val parent: Long,
val seq: Int,
val mimeType: String?,
val decodedBodySize: Long?,
val displayName: String?,
val header: ByteArray?,
val encoding: String?,
val charset: String?,
val dataLocation: Int,
val data: ByteArray?,
val preamble: ByteArray?,
val epilogue: ByteArray?,
val boundary: String?,
val contentId: String?,
val serverExtra: String?
)

View file

@ -33,6 +33,7 @@ class K9MessageStore(
basicPartInfoExtractor,
threadMessageOperations
)
private val copyMessageOperations = CopyMessageOperations(database, attachmentFileManager, threadMessageOperations)
private val moveMessageOperations = MoveMessageOperations(database, threadMessageOperations)
private val flagMessageOperations = FlagMessageOperations(database)
private val retrieveMessageOperations = RetrieveMessageOperations(database)
@ -52,6 +53,12 @@ class K9MessageStore(
return saveMessageOperations.saveLocalMessage(folderId, messageData, existingMessageId)
}
override fun copyMessage(messageId: Long, destinationFolderId: Long): Long {
return copyMessageOperations.copyMessage(messageId, destinationFolderId).also {
localStore.notifyChange()
}
}
override fun moveMessage(messageId: Long, destinationFolderId: Long): Long {
return moveMessageOperations.moveMessage(messageId, destinationFolderId).also {
localStore.notifyChange()

View file

@ -18,7 +18,7 @@ internal class ThreadMessageOperations {
return doMessageThreading(database, destinationFolderId, threadHeaders)
}
private fun getMessageThreadHeaders(database: SQLiteDatabase, messageId: Long): ThreadHeaders {
fun getMessageThreadHeaders(database: SQLiteDatabase, messageId: Long): ThreadHeaders {
return database.rawQuery(
"""
SELECT messages.message_id, message_parts.header

View file

@ -0,0 +1,248 @@
package com.fsck.k9.storage.messages
import com.fsck.k9.mail.crlf
import com.fsck.k9.mailstore.StorageManager
import com.fsck.k9.storage.RobolectricTest
import com.google.common.truth.Truth.assertThat
import okio.buffer
import okio.sink
import okio.source
import org.junit.After
import org.junit.Test
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
private const val ACCOUNT_UUID = "00000000-0000-4000-0000-000000000000"
class CopyMessageOperationsTest : RobolectricTest() {
private val messagePartDirectory = createRandomTempDirectory()
private val sqliteDatabase = createDatabase()
private val storageManager = mock<StorageManager> {
on { getAttachmentDirectory(eq(ACCOUNT_UUID), anyOrNull()) } doReturn messagePartDirectory
}
private val lockableDatabase = createLockableDatabaseMock(sqliteDatabase)
private val attachmentFileManager = AttachmentFileManager(storageManager, ACCOUNT_UUID)
private val threadMessageOperations = ThreadMessageOperations()
private val copyMessageOperations = CopyMessageOperations(
lockableDatabase,
attachmentFileManager,
threadMessageOperations
)
@After
fun tearDown() {
messagePartDirectory.deleteRecursively()
}
@Test
fun `copy message that is part of a thread`() {
val sourceMessagePartId1 = sqliteDatabase.createMessagePart(
seq = 0,
dataLocation = DataLocation.CHILD_PART_CONTAINS_DATA,
mimeType = "multipart/mixed",
boundary = "--boundary",
header = "Message-ID: <msg0002@domain.example>\nIn-Reply-To: <msg0001@domain.example>\n".crlf()
)
val sourceMessagePartId2 = sqliteDatabase.createMessagePart(
seq = 1,
root = sourceMessagePartId1,
parent = sourceMessagePartId1,
mimeType = "text/plain",
dataLocation = DataLocation.IN_DATABASE,
data = "Text part".toByteArray()
)
val sourceMessagePartId3 = sqliteDatabase.createMessagePart(
seq = 2,
root = sourceMessagePartId1,
parent = sourceMessagePartId1,
mimeType = "application/octet-stream",
dataLocation = DataLocation.ON_DISK
)
attachmentFileManager.getAttachmentFile(sourceMessagePartId3).sink().buffer().use { sink ->
sink.writeUtf8("Part contents")
}
val messageId1 = sqliteDatabase.createMessage(
folderId = 1,
empty = true,
messageIdHeader = "<msg0001@domain.example>"
)
val messageId2 = sqliteDatabase.createMessage(
folderId = 1,
empty = false,
messageIdHeader = "<msg0002@domain.example>",
messagePartId = sourceMessagePartId1
)
val messageId3 = sqliteDatabase.createMessage(
folderId = 1,
empty = false,
messageIdHeader = "<msg0003@domain.example>"
)
val threadId1 = sqliteDatabase.createThread(messageId1)
val threadId2 = sqliteDatabase.createThread(messageId2, root = threadId1, parent = threadId1)
val threadId3 = sqliteDatabase.createThread(messageId3, root = threadId1, parent = threadId2)
val destinationMessageId = copyMessageOperations.copyMessage(messageId = messageId2, destinationFolderId = 2)
assertThat(destinationMessageId).isNotIn(setOf(messageId1, messageId2, messageId3))
val threads = sqliteDatabase.readThreads()
assertThat(threads).hasSize(3 + 2)
val destinationMessageThread = threads.first { it.messageId == destinationMessageId }
assertThat(destinationMessageThread.id).isNotIn(setOf(threadId1, threadId2, threadId3))
assertThat(destinationMessageThread.parent).isEqualTo(destinationMessageThread.root)
val destinationRootThread = threads.first { it.id == destinationMessageThread.root }
assertThat(destinationRootThread.messageId).isNotIn(setOf(messageId1, messageId2, messageId3))
assertThat(destinationRootThread.root).isEqualTo(destinationRootThread.id)
assertThat(destinationRootThread.parent).isNull()
val messages = sqliteDatabase.readMessages()
val destinationRootThreadMessage = messages.first { it.id == destinationRootThread.messageId }
assertThat(destinationRootThreadMessage.empty).isEqualTo(1)
assertThat(destinationRootThreadMessage.folderId).isEqualTo(2)
assertThat(destinationRootThreadMessage.messageId).isEqualTo("<msg0001@domain.example>")
val destinationMessage = messages.first { it.id == destinationMessageThread.messageId }
val sourceMessage = messages.first { it.id == messageId2 }
assertThat(destinationMessage).isEqualTo(
sourceMessage.copy(
id = destinationMessageId,
uid = destinationMessage.uid,
folderId = 2,
messagePartId = destinationMessage.messagePartId
)
)
val messageParts = sqliteDatabase.readMessageParts()
assertThat(messageParts).hasSize(3 + 3)
val sourceMessagePart1 = messageParts.first { it.id == sourceMessagePartId1 }
val sourceMessagePart2 = messageParts.first { it.id == sourceMessagePartId2 }
val sourceMessagePart3 = messageParts.first { it.id == sourceMessagePartId3 }
val destinationMessagePart1 = messageParts.first { it.id == destinationMessage.messagePartId }
val destinationMessagePart2 = messageParts.first { it.root == destinationMessage.messagePartId && it.seq == 1 }
val destinationMessagePart3 = messageParts.first { it.root == destinationMessage.messagePartId && it.seq == 2 }
assertThat(destinationMessagePart1).isNotIn(setOf(sourceMessagePart1, sourceMessagePart2, sourceMessagePart3))
assertThat(destinationMessagePart1).isEqualTo(
sourceMessagePart1.copy(
id = destinationMessagePart1.id,
root = destinationMessagePart1.id,
parent = -1
)
)
assertThat(destinationMessagePart2).isNotIn(setOf(sourceMessagePart1, sourceMessagePart2, sourceMessagePart3))
assertThat(destinationMessagePart2).isEqualTo(
sourceMessagePart2.copy(
id = destinationMessagePart2.id,
root = destinationMessagePart1.id,
parent = destinationMessagePart1.id
)
)
assertThat(destinationMessagePart3).isNotIn(setOf(sourceMessagePart1, sourceMessagePart2, sourceMessagePart3))
assertThat(destinationMessagePart3).isEqualTo(
sourceMessagePart3.copy(
id = destinationMessagePart3.id,
root = destinationMessagePart1.id,
parent = destinationMessagePart1.id
)
)
val files = messagePartDirectory.list()?.toList() ?: emptyList()
assertThat(files).hasSize(2)
attachmentFileManager.getAttachmentFile(destinationMessagePart3.id!!).source().buffer().use { source ->
assertThat(source.readUtf8()).isEqualTo("Part contents")
}
}
@Test
fun `copy message into an existing thread`() {
val sourceMessagePartId = sqliteDatabase.createMessagePart(
header = "Message-ID: <msg0002@domain.example>\nIn-Reply-To: <msg0001@domain.example>\n".crlf(),
mimeType = "text/plain",
dataLocation = DataLocation.IN_DATABASE,
data = "Text part".toByteArray()
)
attachmentFileManager.getAttachmentFile(sourceMessagePartId).sink().buffer().use { sink ->
sink.writeUtf8("Part contents")
}
val sourceMessageId = sqliteDatabase.createMessage(
folderId = 1,
empty = false,
messageIdHeader = "<msg0002@domain.example>",
messagePartId = sourceMessagePartId
)
val destinationMessageId = sqliteDatabase.createMessage(
folderId = 2,
empty = true,
messageIdHeader = "<msg0002@domain.example>"
)
val otherDestinationMessageId = sqliteDatabase.createMessage(
folderId = 2,
empty = false,
messageIdHeader = "<msg0003@domain.example>"
)
val destinationThreadId = sqliteDatabase.createThread(destinationMessageId)
val otherDestinationThreadId = sqliteDatabase.createThread(
otherDestinationMessageId,
root = destinationThreadId,
parent = destinationThreadId
)
val resultMessageId = copyMessageOperations.copyMessage(messageId = sourceMessageId, destinationFolderId = 2)
assertThat(resultMessageId).isEqualTo(destinationMessageId)
val threads = sqliteDatabase.readThreads()
assertThat(threads).hasSize(2 + 1)
val destinationThread = threads.first { it.messageId == destinationMessageId }
assertThat(destinationThread.id).isEqualTo(destinationThreadId)
assertThat(destinationThread.parent).isEqualTo(destinationThread.root)
val destinationRootThread = threads.first { it.id == destinationThread.root }
assertThat(destinationRootThread.root).isEqualTo(destinationRootThread.id)
assertThat(destinationRootThread.parent).isNull()
val otherDestinationThread = threads.first { it.id == otherDestinationThreadId }
assertThat(otherDestinationThread.root).isEqualTo(destinationRootThread.id)
assertThat(otherDestinationThread.parent).isEqualTo(destinationThread.id)
val messages = sqliteDatabase.readMessages()
assertThat(messages).hasSize(3 + 1)
val destinationRootThreadMessage = messages.first { it.id == destinationRootThread.messageId }
assertThat(destinationRootThreadMessage.empty).isEqualTo(1)
assertThat(destinationRootThreadMessage.folderId).isEqualTo(2)
assertThat(destinationRootThreadMessage.messageId).isEqualTo("<msg0001@domain.example>")
val destinationMessage = messages.first { it.id == destinationMessageId }
val sourceMessage = messages.first { it.id == sourceMessageId }
assertThat(destinationMessage).isEqualTo(
sourceMessage.copy(
id = destinationMessageId,
uid = destinationMessage.uid,
folderId = 2,
messagePartId = destinationMessage.messagePartId
)
)
val messageParts = sqliteDatabase.readMessageParts()
assertThat(messageParts).hasSize(1 + 1)
val sourceMessagePart = messageParts.first { it.id == sourceMessagePartId }
val destinationMessagePart = messageParts.first { it.id == destinationMessage.messagePartId }
assertThat(destinationMessagePart).isEqualTo(
sourceMessagePart.copy(
id = destinationMessagePart.id,
root = destinationMessagePart.id,
parent = -1
)
)
}
}

View file

@ -10,8 +10,8 @@ import com.fsck.k9.helper.map
fun SQLiteDatabase.createMessagePart(
type: Int = MessagePartType.UNKNOWN,
root: Int? = null,
parent: Int = -1,
root: Long? = null,
parent: Long = -1,
seq: Int = 0,
mimeType: String? = null,
decodedBodySize: Int? = null,
@ -77,7 +77,7 @@ fun SQLiteDatabase.readMessageParts(): List<MessagePartEntry> {
}
}
class MessagePartEntry(
data class MessagePartEntry(
val id: Long?,
val type: Int?,
val root: Long?,
@ -96,7 +96,85 @@ class MessagePartEntry(
val boundary: String?,
val contentId: String?,
val serverExtra: String?
)
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MessagePartEntry
if (id != other.id) return false
if (type != other.type) return false
if (root != other.root) return false
if (parent != other.parent) return false
if (seq != other.seq) return false
if (mimeType != other.mimeType) return false
if (decodedBodySize != other.decodedBodySize) return false
if (displayName != other.displayName) return false
if (header != null) {
if (other.header == null) return false
if (!header.contentEquals(other.header)) return false
} else if (other.header != null) return false
if (encoding != other.encoding) return false
if (charset != other.charset) return false
if (dataLocation != other.dataLocation) return false
if (data != null) {
if (other.data == null) return false
if (!data.contentEquals(other.data)) return false
} else if (other.data != null) return false
if (preamble != other.preamble) return false
if (epilogue != other.epilogue) return false
if (boundary != other.boundary) return false
if (contentId != other.contentId) return false
if (serverExtra != other.serverExtra) return false
return true
}
override fun hashCode(): Int {
var result = id?.hashCode() ?: 0
result = 31 * result + (type ?: 0)
result = 31 * result + (root?.hashCode() ?: 0)
result = 31 * result + (parent?.hashCode() ?: 0)
result = 31 * result + (seq ?: 0)
result = 31 * result + (mimeType?.hashCode() ?: 0)
result = 31 * result + (decodedBodySize ?: 0)
result = 31 * result + (displayName?.hashCode() ?: 0)
result = 31 * result + (header?.contentHashCode() ?: 0)
result = 31 * result + (encoding?.hashCode() ?: 0)
result = 31 * result + (charset?.hashCode() ?: 0)
result = 31 * result + (dataLocation ?: 0)
result = 31 * result + (data?.contentHashCode() ?: 0)
result = 31 * result + (preamble?.hashCode() ?: 0)
result = 31 * result + (epilogue?.hashCode() ?: 0)
result = 31 * result + (boundary?.hashCode() ?: 0)
result = 31 * result + (contentId?.hashCode() ?: 0)
result = 31 * result + (serverExtra?.hashCode() ?: 0)
return result
}
override fun toString(): String {
return "MessagePartEntry(" +
"id=$id, " +
"type=$type, " +
"root=$root, " +
"parent=$parent, " +
"seq=$seq, " +
"mimeType=$mimeType, " +
"decodedBodySize=$decodedBodySize, " +
"displayName=$displayName, " +
"header=${header?.decodeToString()}, " +
"encoding=$encoding, " +
"charset=$charset, " +
"dataLocation=$dataLocation, " +
"data=${data?.decodeToString()}, " +
"preamble=$preamble, " +
"epilogue=$epilogue, " +
"boundary=$boundary, " +
"contentId=$contentId, " +
"serverExtra=$serverExtra)"
}
}
private fun Cursor.getBlobOrNull(columnName: String): ByteArray? {
val columnIndex = getColumnIndex(columnName)