package com.fsck.k9.mailstore.migrations;
import java.io.ByteArrayOutputStream;
import java.io.File;
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;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
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;
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;
class MigrationTo51 {
private static final int MESSAGE_PART_TYPE__UNKNOWN = 0;
private static final int MESSAGE_PART_TYPE__HIDDEN_ATTACHMENT = 6;
private static final int DATA_LOCATION__MISSING = 0;
private static final int DATA_LOCATION__IN_DATABASE = 1;
private static final int DATA_LOCATION__ON_DISK = 2;
/**
* This method converts from the old message table structure to the new one.
*
* This is a complex migration, and ultimately we do not have enough
* information to recreate the mime structure of the original mails.
* What we have:
* - general mail info
* - html_content and text_content data, which is the squashed readable content of the mail
* - a table with message headers
* - attachments
*
* What we need to do:
* - migrate general mail info as-is
* - flag mails as migrated for re-download
* - for each message, recreate a mime structure from its message content and attachments:
* + insert one or both of textContent and htmlContent, depending on mimeType
* + if mimeType is text/plain, text/html or multipart/alternative and no
* attachments are present, just insert that.
* + otherwise, use multipart/mixed, adding attachments after textual content
* + revert content:// URIs in htmlContent to original cid: URIs.
*/
public static void db51MigrateMessageFormat(SQLiteDatabase db, MigrationsHelper migrationsHelper) {
renameOldMessagesTableAndCreateNew(db);
copyMessageMetadataToNewTable(db);
File attachmentDirNew, attachmentDirOld;
Account account = migrationsHelper.getAccount();
attachmentDirNew = StorageManager.getInstance(K9.app).getAttachmentDirectory(
account.getUuid(), account.getLocalStorageProviderId());
attachmentDirOld = renameOldAttachmentDirAndCreateNew(account, attachmentDirNew);
Cursor msgCursor = db.query("messages_old",
new String[] { "id", "flags", "html_content", "text_content", "mime_type", "attachment_count" },
null, null, null, null, null);
try {
Timber.d("migrating %d messages", msgCursor.getCount());
ContentValues cv = new ContentValues();
while (msgCursor.moveToNext()) {
long messageId = msgCursor.getLong(0);
String messageFlags = msgCursor.getString(1);
String htmlContent = msgCursor.getString(2);
String textContent = msgCursor.getString(3);
String mimeType = msgCursor.getString(4);
int attachmentCount = msgCursor.getInt(5);
try {
updateFlagsForMessage(db, messageId, messageFlags, migrationsHelper);
MimeHeader mimeHeader = loadHeaderFromHeadersTable(db, messageId);
MimeStructureState structureState = MimeStructureState.getNewRootState();
boolean messageHadSpecialFormat = false;
// we do not rely on the protocol parameter here but guess by the multipart structure
boolean isMaybePgpMimeEncrypted = attachmentCount == 2
&& MimeUtil.isSameMimeType(mimeType, "multipart/encrypted");
if (isMaybePgpMimeEncrypted) {
MimeStructureState maybeStructureState =
migratePgpMimeEncryptedContent(db, messageId, attachmentDirOld, attachmentDirNew,
mimeHeader, structureState);
if (maybeStructureState != null) {
structureState = maybeStructureState;
messageHadSpecialFormat = true;
}
}
if (!messageHadSpecialFormat) {
boolean isSimpleStructured = attachmentCount == 0 &&
Utility.isAnyMimeType(mimeType, "text/plain", "text/html", "multipart/alternative");
if (isSimpleStructured) {
structureState = migrateSimpleMailContent(db, htmlContent, textContent,
mimeType, mimeHeader, structureState);
} else {
mimeType = "multipart/mixed";
structureState =
migrateComplexMailContent(db, attachmentDirOld, attachmentDirNew, messageId,
htmlContent, textContent, mimeHeader, structureState);
}
}
cv.clear();
cv.put("mime_type", mimeType);
cv.put("message_part_id", structureState.rootPartId);
cv.put("attachment_count", attachmentCount);
db.update("messages", cv, "id = ?", new String[] { Long.toString(messageId) });
} catch (IOException e) {
Timber.e(e, "error inserting into database");
}
}
} finally {
msgCursor.close();
}
cleanUpOldAttachmentDirectory(attachmentDirOld);
dropOldMessagesTable(db);
}
@NonNull
private static File renameOldAttachmentDirAndCreateNew(Account account, File attachmentDirNew) {
File attachmentDirOld = new File(attachmentDirNew.getParent(),
account.getUuid() + ".old_attach-" + System.currentTimeMillis());
boolean moveOk = attachmentDirNew.renameTo(attachmentDirOld);
if (!moveOk) {
// TODO escalate?
Timber.e("Error moving attachment dir! All attachments might be lost!");
}
boolean mkdirOk = attachmentDirNew.mkdir();
if (!mkdirOk) {
// TODO escalate?
Timber.e("Error creating new attachment dir!");
}
return attachmentDirOld;
}
private static void dropOldMessagesTable(SQLiteDatabase db) {
Timber.d("Migration succeeded, dropping old tables.");
db.execSQL("DROP TABLE messages_old");
db.execSQL("DROP TABLE attachments");
db.execSQL("DROP TABLE headers");
}
private static void cleanUpOldAttachmentDirectory(File attachmentDirOld) {
if (!attachmentDirOld.exists()) {
Timber.d("Old attachment directory doesn't exist: %s", attachmentDirOld.getAbsolutePath());
return;
}
for (File file : attachmentDirOld.listFiles()) {
Timber.d("deleting stale attachment file: %s", file.getName());
if (file.exists() && !file.delete()) {
Timber.d("Failed to delete stale attachement file: %s", file.getAbsolutePath());
}
}
Timber.d("deleting old attachment directory");
if (attachmentDirOld.exists() && !attachmentDirOld.delete()) {
Timber.d("Failed to delete old attachement directory: %s", attachmentDirOld.getAbsolutePath());
}
}
private static void copyMessageMetadataToNewTable(SQLiteDatabase db) {
db.execSQL("INSERT INTO messages (" +
"id, deleted, folder_id, uid, subject, date, sender_list, " +
"to_list, cc_list, bcc_list, reply_to_list, attachment_count, " +
"internal_date, message_id, preview, mime_type, " +
"normalized_subject_hash, empty, read, flagged, answered" +
") SELECT " +
"id, deleted, folder_id, uid, subject, date, sender_list, " +
"to_list, cc_list, bcc_list, reply_to_list, attachment_count, " +
"internal_date, message_id, preview, mime_type, " +
"normalized_subject_hash, empty, read, flagged, answered " +
"FROM messages_old");
}
private static void renameOldMessagesTableAndCreateNew(SQLiteDatabase db) {
db.execSQL("ALTER TABLE messages RENAME TO messages_old");
db.execSQL("CREATE TABLE messages (" +
"id INTEGER PRIMARY KEY, " +
"deleted INTEGER default 0, " +
"folder_id INTEGER, " +
"uid TEXT, " +
"subject TEXT, " +
"date INTEGER, " +
"flags TEXT, " +
"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, "+
"normalized_subject_hash INTEGER, " +
"empty INTEGER default 0, " +
"read INTEGER default 0, " +
"flagged INTEGER default 0, " +
"answered INTEGER default 0, " +
"forwarded INTEGER default 0, " +
"message_part_id INTEGER" +
")");
db.execSQL("CREATE TABLE message_parts (" +
"id INTEGER PRIMARY KEY, " +
"type INTEGER NOT NULL, " +
"root INTEGER, " +
"parent INTEGER NOT NULL, " +
"seq INTEGER NOT NULL, " +
"mime_type TEXT, " +
"decoded_body_size INTEGER, " +
"display_name TEXT, " +
"header TEXT, " +
"encoding TEXT, " +
"charset TEXT, " +
"data_location INTEGER NOT NULL, " +
"data BLOB, " +
"preamble TEXT, " +
"epilogue TEXT, " +
"boundary TEXT, " +
"content_id TEXT, " +
"server_extra TEXT" +
")");
db.execSQL("CREATE TRIGGER set_message_part_root " +
"AFTER INSERT ON message_parts " +
"BEGIN " +
"UPDATE message_parts SET root=id WHERE root IS NULL AND ROWID = NEW.ROWID; " +
"END");
}
@Nullable
private static MimeStructureState migratePgpMimeEncryptedContent(SQLiteDatabase db, long messageId,
File attachmentDirOld, File attachmentDirNew, MimeHeader mimeHeader, MimeStructureState structureState) {
Timber.d("Attempting to migrate multipart/encrypted as pgp/mime");
// we only handle attachment count == 2 here, so simply sorting application/pgp-encrypted
// to the front (and application/octet-stream second) should suffice.
String orderBy = "(mime_type LIKE 'application/pgp-encrypted') DESC";
Cursor cursor = db.query("attachments",
new String[] {
"id", "size", "name", "mime_type", "store_data",
"content_uri", "content_id", "content_disposition"
},
"message_id = ?", new String[] { Long.toString(messageId) }, null, null, orderBy);
try {
if (cursor.getCount() != 2) {
Timber.e("Found multipart/encrypted but bad number of attachments, handling as regular mail");
return null;
}
cursor.moveToFirst();
long firstPartId = cursor.getLong(0);
int firstPartSize = cursor.getInt(1);
String firstPartName = cursor.getString(2);
String firstPartMimeType = cursor.getString(3);
String firstPartStoreData = cursor.getString(4);
String firstPartContentUriString = cursor.getString(5);
if (!MimeUtil.isSameMimeType(firstPartMimeType, "application/pgp-encrypted")) {
Timber.e("First part in multipart/encrypted wasn't application/pgp-encrypted, " +
"not handling as pgp/mime");
return null;
}
cursor.moveToNext();
long secondPartId = cursor.getLong(0);
int secondPartSize = cursor.getInt(1);
String secondPartName = cursor.getString(2);
String secondPartMimeType = cursor.getString(3);
String secondPartStoreData = cursor.getString(4);
String secondPartContentUriString = cursor.getString(5);
if (!MimeUtil.isSameMimeType(secondPartMimeType, "application/octet-stream")) {
Timber.e("First part in multipart/encrypted wasn't application/octet-stream, not handling as pgp/mime");
return null;
}
String boundary = MimeUtility.getHeaderParameter(
mimeHeader.getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE), "boundary");
if (TextUtils.isEmpty(boundary)) {
boundary = MimeUtil.createUniqueBoundary();
}
mimeHeader.setHeader(MimeHeader.HEADER_CONTENT_TYPE,
String.format("multipart/encrypted; boundary=\"%s\"; protocol=\"application/pgp-encrypted\"", boundary));
ContentValues cv = new ContentValues();
cv.put("type", MESSAGE_PART_TYPE__UNKNOWN);
cv.put("data_location", DATA_LOCATION__IN_DATABASE);
cv.put("mime_type", "multipart/encrypted");
cv.put("header", mimeHeader.toString());
cv.put("boundary", boundary);
structureState.applyValues(cv);
long rootMessagePartId = db.insertOrThrow("message_parts", null, cv);
structureState = structureState.nextMultipartChild(rootMessagePartId);
structureState =
insertMimeAttachmentPart(db, attachmentDirOld, attachmentDirNew, structureState, firstPartId,
firstPartSize, firstPartName, "application/pgp-encrypted", firstPartStoreData,
firstPartContentUriString, null, null);
structureState =
insertMimeAttachmentPart(db, attachmentDirOld, attachmentDirNew, structureState, secondPartId,
secondPartSize, secondPartName, "application/octet-stream", secondPartStoreData,
secondPartContentUriString, null, null);
} finally {
cursor.close();
}
return structureState;
}
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");
String boundary = MimeUtility.getHeaderParameter(
mimeHeader.getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE), "boundary");
if (TextUtils.isEmpty(boundary)) {
boundary = MimeUtil.createUniqueBoundary();
}
mimeHeader.setHeader(MimeHeader.HEADER_CONTENT_TYPE,
String.format("multipart/mixed; boundary=\"%s\";", boundary));
ContentValues cv = new ContentValues();
cv.put("type", MESSAGE_PART_TYPE__UNKNOWN);
cv.put("data_location", DATA_LOCATION__IN_DATABASE);
cv.put("mime_type", "multipart/mixed");
cv.put("header", mimeHeader.toString());
cv.put("boundary", boundary);
structureState.applyValues(cv);
long rootMessagePartId = db.insertOrThrow("message_parts", null, cv);
structureState = structureState.nextMultipartChild(rootMessagePartId);
if (htmlContent != null) {
htmlContent = replaceContentUriWithContentIdInHtmlPart(db, messageId, htmlContent);
}
if (textContent != null && htmlContent != null) {
structureState = insertBodyAsMultipartAlternative(db, structureState, null, textContent, htmlContent);
structureState = structureState.popParent();
} else if (textContent != null) {
structureState = insertTextualPartIntoDatabase(db, structureState, null, textContent, false);
} else if (htmlContent != null) {
structureState = insertTextualPartIntoDatabase(db, structureState, null, htmlContent, true);
}
structureState = insertAttachments(db, attachmentDirOld, attachmentDirNew, messageId, structureState);
return structureState;
}
private static String replaceContentUriWithContentIdInHtmlPart(
SQLiteDatabase db, long messageId, String htmlContent) {
Cursor cursor = db.query("attachments", new String[] { "content_uri", "content_id" },
"content_id IS NOT NULL AND message_id = ?", new String[] { Long.toString(messageId) }, null, null, null);
try {
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);
}
} finally {
cursor.close();
}
return htmlContent;
}
private static MimeStructureState migrateSimpleMailContent(SQLiteDatabase db, String htmlContent,
String textContent, String mimeType, MimeHeader mimeHeader, MimeStructureState structureState)
throws IOException {
Timber.d("Processing mail with simple structure");
if (MimeUtil.isSameMimeType(mimeType, "text/plain")) {
return insertTextualPartIntoDatabase(db, structureState, mimeHeader, textContent, false);
} else if (MimeUtil.isSameMimeType(mimeType, "text/html")) {
return insertTextualPartIntoDatabase(db, structureState, mimeHeader, htmlContent, true);
} else if (MimeUtil.isSameMimeType(mimeType, "multipart/alternative")) {
return insertBodyAsMultipartAlternative(db, structureState, mimeHeader, textContent, htmlContent);
} else {
throw new IllegalStateException("migrateSimpleMailContent cannot handle mimeType " + mimeType);
}
}
private static MimeStructureState insertAttachments(SQLiteDatabase db, File attachmentDirOld, File attachmentDirNew,
long messageId, MimeStructureState structureState) {
Cursor cursor = db.query("attachments",
new String[] {
"id", "size", "name", "mime_type", "store_data",
"content_uri", "content_id", "content_disposition"
},
"message_id = ?", new String[] { Long.toString(messageId) }, null, null, null);
try {
while (cursor.moveToNext()) {
long id = cursor.getLong(0);
int size = cursor.getInt(1);
String name = cursor.getString(2);
String mimeType = cursor.getString(3);
String storeData = cursor.getString(4);
String contentUriString = cursor.getString(5);
String contentId = cursor.getString(6);
String contentDisposition = cursor.getString(7);
structureState =
insertMimeAttachmentPart(db, attachmentDirOld, attachmentDirNew, structureState, id, size, name,
mimeType, storeData, contentUriString, contentId, contentDisposition);
}
} finally {
cursor.close();
}
return structureState;
}
private static MimeStructureState insertMimeAttachmentPart(SQLiteDatabase db, File attachmentDirOld,
File attachmentDirNew, MimeStructureState structureState, long id, int size, String name, String mimeType,
String storeData, String contentUriString, String contentId, String contentDisposition) {
Timber.d("processing attachment %d, %s, %s, %s, %s",
id,
name,
mimeType,
storeData,
contentUriString);
if (contentDisposition == null) {
contentDisposition = "attachment";
}
MimeHeader mimeHeader = new MimeHeader();
mimeHeader.setHeader(MimeHeader.HEADER_CONTENT_TYPE,
String.format("%s;\r\n name=\"%s\"", mimeType, name));
mimeHeader.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION,
String.format(Locale.US, "%s;\r\n filename=\"%s\";\r\n size=%d",
contentDisposition, name, size)); // TODO: Should use encoded word defined in RFC 2231.
if (contentId != null) {
mimeHeader.setHeader(MimeHeader.HEADER_CONTENT_ID, contentId);
}
boolean hasData = contentUriString != null;
File attachmentFileToMove;
if (hasData) {
try {
Uri contentUri = Uri.parse(contentUriString);
List<String> pathSegments = contentUri.getPathSegments();
String attachmentId = pathSegments.get(1);
boolean isMatchingAttachmentId = Long.parseLong(attachmentId) == id;
File attachmentFile = new File(attachmentDirOld, attachmentId);
boolean isExistingAttachmentFile = attachmentFile.exists();
if (!isMatchingAttachmentId) {
Timber.e("mismatched attachment id. mark as missing");
attachmentFileToMove = null;
} else if (!isExistingAttachmentFile) {
Timber.e("attached file doesn't exist. mark as missing");
attachmentFileToMove = null;
} else {
attachmentFileToMove = attachmentFile;
}
} catch (Exception e) {
// anything here fails, conservatively assume the data doesn't exist
attachmentFileToMove = null;
}
} else {
attachmentFileToMove = null;
}
if (attachmentFileToMove == null) {
Timber.d("matching attachment is in local cache");
}
boolean hasContentTypeAndIsInline = !TextUtils.isEmpty(contentId) && "inline".equalsIgnoreCase(contentDisposition);
int messageType = hasContentTypeAndIsInline ?
MESSAGE_PART_TYPE__HIDDEN_ATTACHMENT : MESSAGE_PART_TYPE__UNKNOWN;
ContentValues cv = new ContentValues();
cv.put("type", messageType);
cv.put("mime_type", mimeType);
cv.put("decoded_body_size", size);
cv.put("display_name", name);
cv.put("header", mimeHeader.toString());
cv.put("encoding", MimeUtil.ENC_BINARY);
cv.put("data_location", attachmentFileToMove != null ? DATA_LOCATION__ON_DISK : DATA_LOCATION__MISSING);
cv.put("content_id", contentId);
cv.put("server_extra", storeData);
structureState.applyValues(cv);
long partId = db.insertOrThrow("message_parts", null, cv);
structureState = structureState.nextChild(partId);
if (attachmentFileToMove != null) {
boolean moveOk = attachmentFileToMove.renameTo(new File(attachmentDirNew, Long.toString(partId)));
if (!moveOk) {
Timber.e("Moving attachment to new dir failed!");
}
}
return structureState;
}
private static void updateFlagsForMessage(SQLiteDatabase db, long messageId, String messageFlags,
MigrationsHelper migrationsHelper) {
List<Flag> extraFlags = new ArrayList<>();
if (messageFlags != null && messageFlags.length() > 0) {
String[] flags = messageFlags.split(",");
for (String flagStr : flags) {
try {
Flag flag = Flag.valueOf(flagStr);
extraFlags.add(flag);
} catch (Exception e) {
// Ignore bad flags
}
}
}
extraFlags.add(Flag.X_MIGRATED_FROM_V50);
String flagsString = migrationsHelper.serializeFlags(extraFlags);
db.execSQL("UPDATE messages SET flags = ? WHERE id = ?", new Object[] { flagsString, messageId } );
}
private static MimeStructureState insertBodyAsMultipartAlternative(SQLiteDatabase db,
MimeStructureState structureState, MimeHeader mimeHeader,
String textContent, String htmlContent) throws IOException {
if (mimeHeader == null) {
mimeHeader = new MimeHeader();
}
String boundary = MimeUtility.getHeaderParameter(
mimeHeader.getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE), "boundary");
if (TextUtils.isEmpty(boundary)) {
boundary = MimeUtil.createUniqueBoundary();
}
mimeHeader.setHeader(MimeHeader.HEADER_CONTENT_TYPE,
String.format("multipart/alternative; boundary=\"%s\";", boundary));
int dataLocation = textContent != null || htmlContent != null
? DATA_LOCATION__IN_DATABASE : DATA_LOCATION__MISSING;
ContentValues cv = new ContentValues();
cv.put("type", MESSAGE_PART_TYPE__UNKNOWN);
cv.put("data_location", dataLocation);
cv.put("mime_type", "multipart/alternative");
cv.put("header", mimeHeader.toString());
cv.put("boundary", boundary);
structureState.applyValues(cv);
long multipartAlternativePartId = db.insertOrThrow("message_parts", null, cv);
structureState = structureState.nextMultipartChild(multipartAlternativePartId);
if (textContent != null) {
structureState = insertTextualPartIntoDatabase(db, structureState, null, textContent, false);
}
if (htmlContent != null) {
structureState = insertTextualPartIntoDatabase(db, structureState, null, htmlContent, true);
}
return structureState;
}
private static MimeStructureState insertTextualPartIntoDatabase(SQLiteDatabase db, MimeStructureState structureState,
MimeHeader mimeHeader, String content, boolean isHtml) throws IOException {
if (mimeHeader == null) {
mimeHeader = new MimeHeader();
}
mimeHeader.setHeader(MimeHeader.HEADER_CONTENT_TYPE,
isHtml ? "text/html; charset=\"utf-8\"" : "text/plain; charset=\"utf-8\"");
mimeHeader.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, MimeUtil.ENC_QUOTED_PRINTABLE);
byte[] contentBytes;
int decodedBodySize;
int dataLocation;
if (content != null) {
ByteArrayOutputStream contentOutputStream = new ByteArrayOutputStream();
QuotedPrintableOutputStream quotedPrintableOutputStream =
new QuotedPrintableOutputStream(contentOutputStream, false);
quotedPrintableOutputStream.write(content.getBytes());
quotedPrintableOutputStream.flush();
dataLocation = DATA_LOCATION__IN_DATABASE;
contentBytes = contentOutputStream.toByteArray();
decodedBodySize = content.length();
} else {
dataLocation = DATA_LOCATION__MISSING;
contentBytes = null;
decodedBodySize = 0;
}
ContentValues cv = new ContentValues();
cv.put("type", MESSAGE_PART_TYPE__UNKNOWN);
cv.put("data_location", dataLocation);
cv.put("mime_type", isHtml ? "text/html" : "text/plain");
cv.put("header", mimeHeader.toString());
cv.put("data", contentBytes);
cv.put("decoded_body_size", decodedBodySize);
cv.put("encoding", MimeUtil.ENC_QUOTED_PRINTABLE);
cv.put("charset", "utf-8");
structureState.applyValues(cv);
long partId = db.insertOrThrow("message_parts", null, cv);
return structureState.nextChild(partId);
}
private static MimeHeader loadHeaderFromHeadersTable(SQLiteDatabase db, long messageId) {
Cursor headersCursor = db.query("headers",
new String[] { "name", "value" },
"message_id = ?", new String[] { Long.toString(messageId) }, null, null, null);
try {
MimeHeader mimeHeader = new MimeHeader();
while (headersCursor.moveToNext()) {
String name = headersCursor.getString(0);
String value = headersCursor.getString(1);
mimeHeader.addHeader(name, value);
}
return mimeHeader;
} finally {
headersCursor.close();
}
}
/**
* Objects of this class hold immutable information on a database position for
* one part of the mime structure of a message.
*
* An object of this class must be passed to and returned by every operation
* which inserts mime parts into the database. Each mime part which is inserted
* must call the {#applyValues()} method on its ContentValues, then obtain the
* next state object by calling the appropriate next*() method.
*
* While the data carried by this object is immutable, it contains some state
* to ensure that the operations are called correctly and in order.
*
* Because the insertion operations required for the database migration are
* strictly linear, we do not require a more complex stack-based data structure
* here.
*/
@VisibleForTesting
static class MimeStructureState {
private final Long rootPartId;
private final Long prevParentId;
private final long parentId;
private final int nextOrder;
// just some diagnostic state to make sure all operations are called in order
private boolean isValuesApplied;
private boolean isStateAdvanced;
private MimeStructureState(Long rootPartId, Long prevParentId, long parentId, int nextOrder) {
this.rootPartId = rootPartId;
this.prevParentId = prevParentId;
this.parentId = parentId;
this.nextOrder = nextOrder;
}
public static MimeStructureState getNewRootState() {
return new MimeStructureState(null, null, -1, 0);
}
public MimeStructureState nextChild(long newPartId) {
if (!isValuesApplied || isStateAdvanced) {
throw new IllegalStateException("next* methods must only be called once");
}
isStateAdvanced = true;
if (rootPartId == null) {
return new MimeStructureState(newPartId, null, -1, nextOrder+1);
}
return new MimeStructureState(rootPartId, prevParentId, parentId, nextOrder+1);
}
public MimeStructureState nextMultipartChild(long newPartId) {
if (!isValuesApplied || isStateAdvanced) {
throw new IllegalStateException("next* methods must only be called once");
}
isStateAdvanced = true;
if (rootPartId == null) {
return new MimeStructureState(newPartId, parentId, newPartId, nextOrder+1);
}
return new MimeStructureState(rootPartId, parentId, newPartId, nextOrder+1);
}
public void applyValues(ContentValues cv) {
if (isValuesApplied || isStateAdvanced) {
throw new IllegalStateException("applyValues must be called exactly once, after a call to next*");
}
if (rootPartId != null && parentId == -1L) {
throw new IllegalStateException("applyValues must not be called after a root nextChild call");
}
isValuesApplied = true;
if (rootPartId != null) {
cv.put("root", rootPartId);
}
cv.put("parent", parentId);
cv.put("seq", nextOrder);
}
public MimeStructureState popParent() {
if (prevParentId == null) {
throw new IllegalStateException("popParent must only be called if parent depth is >= 2");
}
return new MimeStructureState(rootPartId, null, prevParentId, nextOrder);
}
}
}