0
0
mirror of https://github.com/thunderbird/thunderbird-android.git synced 2024-09-20 04:02:14 +02:00

Merge pull request #1048 from k9mail/mime-migration

Migrate old messages to new mime format
This commit is contained in:
cketti 2016-02-13 01:25:17 +01:00
commit 66930c0081
9 changed files with 1621 additions and 7 deletions

View File

@ -48,4 +48,13 @@ public enum Flag {
* Indicates that the copy of a message to the Sent folder has started.
*/
X_REMOTE_COPY_STARTED,
/**
* Messages with this flag have been migrated from database version 50 or earlier.
* This earlier database format did not preserve the original mime structure of a
* mail, which means messages migrated to the newer database structure may be
* incomplete or broken.
* TODO Messages with this flag should be redownloaded, if possible.
*/
X_MIGRATED_FROM_V50,
}

View File

@ -75,6 +75,19 @@ public class MimeHeader implements Cloneable {
mFields.removeAll(removeFields);
}
public String toString() {
StringBuilder builder = new StringBuilder();
for (Field field : mFields) {
if (field.hasRawData()) {
builder.append(field.getRaw());
} else {
writeNameValueField(builder, field);
}
builder.append('\r').append('\n');
}
return builder.toString();
}
public void writeTo(OutputStream out) throws IOException {
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
for (Field field : mFields) {
@ -105,6 +118,23 @@ public class MimeHeader implements Cloneable {
writer.write(value);
}
private void writeNameValueField(StringBuilder builder, Field field) {
String value = field.getValue();
if (hasToBeEncoded(value)) {
Charset charset = null;
if (mCharset != null) {
charset = Charset.forName(mCharset);
}
value = EncoderUtil.encodeEncodedWord(field.getValue(), charset);
}
builder.append(field.getName());
builder.append(": ");
builder.append(value);
}
// encode non printable characters except LF/CR/TAB codes.
private boolean hasToBeEncoded(String text) {
for (int i = 0; i < text.length(); i++) {

View File

@ -14,6 +14,7 @@ import android.widget.EditText;
import android.widget.TextView;
import com.fsck.k9.K9;
import org.apache.james.mime4j.util.MimeUtil;
import java.nio.charset.Charset;
import java.util.ArrayList;
@ -48,6 +49,15 @@ public class Utility {
return false;
}
public static boolean isAnyMimeType(String o, String... a) {
for (String element : a) {
if (MimeUtil.isSameMimeType(element, o)) {
return true;
}
}
return false;
}
public static boolean arrayContainsAny(Object[] a, Object... o) {
for (Object element : a) {
if (arrayContains(o, element)) {

View File

@ -65,7 +65,7 @@ public class LocalFolder extends Folder<LocalMessage> implements Serializable {
private static final long serialVersionUID = -1973296520918624767L;
private static final int MAX_BODY_SIZE_FOR_DATABASE = 16 * 1024;
private static final long INVALID_MESSAGE_PART_ID = -1;
static final long INVALID_MESSAGE_PART_ID = -1;
private final LocalStore localStore;
@ -2038,7 +2038,7 @@ public class LocalFolder extends Folder<LocalMessage> implements Serializable {
}
// Note: The contents of the 'message_parts' table depend on these values.
private static class MessagePartType {
static class MessagePartType {
static final int UNKNOWN = 0;
static final int ALTERNATIVE_PLAIN = 1;
static final int ALTERNATIVE_HTML = 2;

View File

@ -805,7 +805,7 @@ public class LocalStore extends Store implements Serializable {
}
String serializeFlags(Iterable<Flag> flags) {
static String serializeFlags(Iterable<Flag> flags) {
List<Flag> extraFlags = new ArrayList<>();
for (Flag flag : flags) {

View File

@ -1,14 +1,24 @@
package com.fsck.k9.mailstore;
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.content.SharedPreferences;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.Log;
import com.fsck.k9.Account;
@ -18,6 +28,13 @@ import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Folder;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.internet.MimeHeader;
import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mailstore.LocalFolder.DataLocation;
import com.fsck.k9.mailstore.LocalFolder.MessagePartType;
import org.apache.james.mime4j.codec.QuotedPrintableOutputStream;
import org.apache.james.mime4j.util.MimeUtil;
class StoreSchemaDefinition implements LockableDatabase.SchemaDefinition {
private final LocalStore localStore;
@ -92,7 +109,7 @@ class StoreSchemaDefinition implements LockableDatabase.SchemaDefinition {
case 44:
db45ChangeThreadingIndexes(db);
case 45:
db46AddMessagesFlagColumns(db, localStore);
db46AddMessagesFlagColumns(db);
case 46:
db47CreateThreadsTable(db);
case 47:
@ -102,7 +119,7 @@ class StoreSchemaDefinition implements LockableDatabase.SchemaDefinition {
case 49:
db50FoldersAddNotifyClassColumn(db, localStore);
case 50:
throw new IllegalStateException("Database upgrade not supported yet!");
db51MigrateMessageFormat(db, localStore);
case 51:
db52AddMoreMessagesColumnToFoldersTable(db);
case 52:
@ -124,7 +141,682 @@ class StoreSchemaDefinition implements LockableDatabase.SchemaDefinition {
}
}
/** 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);
}
}
/** 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.
*
*/
private static void db51MigrateMessageFormat(SQLiteDatabase db, LocalStore localStore) {
renameOldMessagesTableAndCreateNew(db);
copyMessageMetadataToNewTable(db);
File attachmentDirNew, attachmentDirOld;
Account account = localStore.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 {
Log.d(K9.LOG_TAG, "migrating " + msgCursor.getCount() + " messages");
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);
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) {
Log.e(K9.LOG_TAG, "error inserting into database", e);
}
}
} 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?
Log.e(K9.LOG_TAG, "Error moving attachment dir! All attachments might be lost!");
}
boolean mkdirOk = attachmentDirNew.mkdir();
if (!mkdirOk) {
// TODO escalate?
Log.e(K9.LOG_TAG, "Error creating new attachment dir!");
}
return attachmentDirOld;
}
private static void dropOldMessagesTable(SQLiteDatabase db) {
Log.d(K9.LOG_TAG, "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) {
for (File file : attachmentDirOld.listFiles()) {
Log.d(K9.LOG_TAG, "deleting stale attachment file: " + file.getName());
file.delete();
}
Log.d(K9.LOG_TAG, "deleting old attachment directory");
attachmentDirOld.delete();
}
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) {
Log.d(K9.LOG_TAG, "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) {
Log.e(K9.LOG_TAG, "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")) {
Log.e(K9.LOG_TAG,
"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")) {
Log.e(K9.LOG_TAG,
"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", MessagePartType.UNKNOWN);
cv.put("data_location", DataLocation.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 {
Log.d(K9.LOG_TAG, "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", MessagePartType.UNKNOWN);
cv.put("data_location", DataLocation.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 {
Log.d(K9.LOG_TAG, "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) {
if (K9.DEBUG) {
Log.d(K9.LOG_TAG, "processing attachment " + 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) {
Log.e(K9.LOG_TAG, "mismatched attachment id. mark as missing");
attachmentFileToMove = null;
} else if (!isExistingAttachmentFile) {
Log.e(K9.LOG_TAG, "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 (K9.DEBUG && attachmentFileToMove == null) {
Log.d(K9.LOG_TAG, "matching attachment is in local cache");
}
boolean hasContentTypeAndIsInline = !TextUtils.isEmpty(contentId) && "inline".equalsIgnoreCase(contentDisposition);
int messageType = hasContentTypeAndIsInline ? MessagePartType.HIDDEN_ATTACHMENT : MessagePartType.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 ? DataLocation.ON_DISK : DataLocation.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) {
Log.e(K9.LOG_TAG, "Moving attachment to new dir failed!");
}
}
return structureState;
}
private static void updateFlagsForMessage(SQLiteDatabase db, long messageId, String messageFlags) {
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 = LocalStore.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));
ContentValues cv = new ContentValues();
cv.put("type", MessagePartType.UNKNOWN);
cv.put("data_location", DataLocation.IN_DATABASE);
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 (!TextUtils.isEmpty(textContent)) {
structureState = insertTextualPartIntoDatabase(db, structureState, null, textContent, false);
}
if (!TextUtils.isEmpty(htmlContent)) {
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);
ByteArrayOutputStream contentOutputStream = new ByteArrayOutputStream();
QuotedPrintableOutputStream quotedPrintableOutputStream =
new QuotedPrintableOutputStream(contentOutputStream, false);
quotedPrintableOutputStream.write(content.getBytes());
quotedPrintableOutputStream.flush();
byte[] contentBytes = contentOutputStream.toByteArray();
ContentValues cv = new ContentValues();
cv.put("type", MessagePartType.UNKNOWN);
cv.put("data_location", DataLocation.IN_DATABASE);
cv.put("mime_type", isHtml ? "text/html" : "text/plain");
cv.put("header", mimeHeader.toString());
cv.put("data", contentBytes);
cv.put("decoded_body_size", content.length());
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();
}
}
private static void dbCreateDatabaseFromScratch(SQLiteDatabase db) {
db.execSQL("DROP TABLE IF EXISTS folders");
db.execSQL("CREATE TABLE folders (" +
"id INTEGER PRIMARY KEY," +
@ -501,7 +1193,7 @@ class StoreSchemaDefinition implements LockableDatabase.SchemaDefinition {
}
}
private static void db46AddMessagesFlagColumns(SQLiteDatabase db, LocalStore localStore) {
private static void db46AddMessagesFlagColumns(SQLiteDatabase db) {
db.execSQL("ALTER TABLE messages ADD read INTEGER default 0");
db.execSQL("ALTER TABLE messages ADD flagged INTEGER default 0");
db.execSQL("ALTER TABLE messages ADD answered INTEGER default 0");
@ -570,7 +1262,7 @@ class StoreSchemaDefinition implements LockableDatabase.SchemaDefinition {
}
cv.put("flags", localStore.serializeFlags(extraFlags));
cv.put("flags", LocalStore.serializeFlags(extraFlags));
cv.put("read", read);
cv.put("flagged", flagged);
cv.put("answered", answered);

View File

@ -0,0 +1,150 @@
package com.fsck.k9.mailstore;
import android.content.ContentValues;
import com.fsck.k9.mailstore.StoreSchemaDefinition.MimeStructureState;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@RunWith(RobolectricTestRunner.class) // required for ContentValues
@Config(manifest = "src/main/AndroidManifest.xml", sdk = 21)
public class MigrationMimeStructureStateTest {
@Test(expected = IllegalStateException.class)
public void init_popParent_shouldCrash() throws Exception {
MimeStructureState state = MimeStructureState.getNewRootState();
state.popParent();
}
@Test(expected = IllegalStateException.class)
public void init_apply_apply_shouldCrash() throws Exception {
MimeStructureState state = MimeStructureState.getNewRootState();
ContentValues cv = new ContentValues();
state.applyValues(cv);
state.applyValues(cv);
}
@Test(expected = IllegalStateException.class)
public void init_nextchild_shouldCrash() throws Exception {
MimeStructureState state = MimeStructureState.getNewRootState();
state.nextChild(1);
}
@Test(expected = IllegalStateException.class)
public void init_nextmulti_shouldCrash() throws Exception {
MimeStructureState state = MimeStructureState.getNewRootState();
state.nextMultipartChild(1);
}
@Test(expected = IllegalStateException.class)
public void init_apply_nextmulti_nextchild_shouldCrash() throws Exception {
MimeStructureState state = MimeStructureState.getNewRootState();
ContentValues cv = new ContentValues();
state.applyValues(cv);
state.nextMultipartChild(1);
state.nextChild(1);
}
@Test(expected = IllegalStateException.class)
public void init_apply_nextchild_nextmulti_shouldCrash() throws Exception {
MimeStructureState state = MimeStructureState.getNewRootState();
ContentValues cv = new ContentValues();
state.applyValues(cv);
state.nextChild(1);
state.nextMultipartChild(1);
}
@Test
public void init_apply_shouldYieldStartValues() throws Exception {
MimeStructureState state = MimeStructureState.getNewRootState();
ContentValues cv = new ContentValues();
state.applyValues(cv);
Assert.assertEquals(-1L, cv.get("parent"));
Assert.assertEquals(0, cv.get("seq"));
Assert.assertEquals(2, cv.size());
}
@Test(expected = IllegalStateException.class)
public void init_apply_nextchild_apply_shouldCrash() throws Exception {
MimeStructureState state = MimeStructureState.getNewRootState();
ContentValues cv = new ContentValues();
state.applyValues(cv);
state = state.nextChild(123);
cv.clear();
state.applyValues(cv);
}
@Test
public void init_apply_nextmulti_apply_shouldYieldMultipartChildValues() throws Exception {
MimeStructureState state = MimeStructureState.getNewRootState();
ContentValues cv = new ContentValues();
state.applyValues(cv);
state = state.nextMultipartChild(123);
cv.clear();
state.applyValues(cv);
Assert.assertEquals(123L, cv.get("root"));
Assert.assertEquals(123L, cv.get("parent"));
Assert.assertEquals(1, cv.get("seq"));
Assert.assertEquals(3, cv.size());
}
@Test
public void init_apply_nextmulti_apply_nextmulti_apply_shouldYieldSecondMultipartChildValues() throws Exception {
MimeStructureState state = MimeStructureState.getNewRootState();
ContentValues cv = new ContentValues();
state.applyValues(cv);
state = state.nextMultipartChild(123);
cv.clear();
state.applyValues(cv);
state = state.nextMultipartChild(456);
cv.clear();
state.applyValues(cv);
Assert.assertEquals(123L, cv.get("root"));
Assert.assertEquals(456L, cv.get("parent"));
Assert.assertEquals(2, cv.get("seq"));
Assert.assertEquals(3, cv.size());
}
@Test
public void init_apply_nextmulti_apply_pop_apply_shouldYieldFirstParentIdValues() throws Exception {
MimeStructureState state = MimeStructureState.getNewRootState();
ContentValues cv = new ContentValues();
state.applyValues(cv);
state = state.nextMultipartChild(123);
cv.clear();
state.applyValues(cv);
state = state.nextMultipartChild(456);
state = state.popParent();
cv.clear();
state.applyValues(cv);
Assert.assertEquals(123L, cv.get("root"));
Assert.assertEquals(123L, cv.get("parent"));
Assert.assertEquals(2, cv.get("seq"));
Assert.assertEquals(3, cv.size());
}
}

View File

@ -0,0 +1,723 @@
package com.fsck.k9.mailstore;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Collections;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import com.fsck.k9.Account;
import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
import com.fsck.k9.mail.BodyPart;
import com.fsck.k9.mail.FetchProfile;
import com.fsck.k9.mail.Multipart;
import com.fsck.k9.mail.internet.MessageExtractor;
import com.fsck.k9.mail.internet.MimeHeader;
import com.fsck.k9.mail.internet.MimeUtility;
import org.apache.commons.io.IOUtils;
import org.apache.james.mime4j.util.MimeUtil;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.openintents.openpgp.util.OpenPgpUtils;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLog;
import org.robolectric.shadows.ShadowSQLiteConnection;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = "src/main/AndroidManifest.xml", sdk = 21)
public class MigrationTest {
Account account;
File databaseFile;
File attachmentDir;
@Before
public void setUp() throws Exception {
K9.DEBUG = true;
ShadowLog.stream = System.out;
ShadowSQLiteConnection.reset();
account = Preferences.getPreferences(RuntimeEnvironment.application).newAccount();
StorageManager storageManager = StorageManager.getInstance(RuntimeEnvironment.application);
databaseFile = storageManager.getDatabase(account.getUuid(), account.getLocalStorageProviderId());
Assert.assertTrue(databaseFile.getParentFile().isDirectory() || databaseFile.getParentFile().mkdir());
attachmentDir = StorageManager.getInstance(RuntimeEnvironment.application).getAttachmentDirectory(
account.getUuid(), account.getLocalStorageProviderId());
Assert.assertTrue(attachmentDir.isDirectory() || attachmentDir.mkdir());
}
private SQLiteDatabase createV50Database() {
SQLiteDatabase db = RuntimeEnvironment.application.openOrCreateDatabase(databaseFile.getName(),
Context.MODE_PRIVATE, null);
String[] v50SchemaSql = new String[] {
"CREATE TABLE folders (id INTEGER PRIMARY KEY, name TEXT, last_updated INTEGER, unread_count INTEGER," +
"visible_limit INTEGER, status TEXT, push_state TEXT, last_pushed INTEGER," +
"flagged_count INTEGER default 0, integrate INTEGER, top_group INTEGER, poll_class TEXT," +
"push_class TEXT, display_class TEXT, notify_class TEXT);",
"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, html_content TEXT, text_content TEXT," +
"attachment_count INTEGER, internal_date INTEGER, message_id TEXT, preview TEXT," +
"mime_type TEXT, normalized_subject_hash INTEGER, empty INTEGER, read INTEGER default 0," +
"flagged INTEGER default 0, answered INTEGER default 0, forwarded INTEGER default 0);",
"CREATE TABLE headers (id INTEGER PRIMARY KEY, message_id INTEGER, name TEXT, value TEXT);",
"CREATE TABLE threads (id INTEGER PRIMARY KEY, message_id INTEGER, root INTEGER, parent INTEGER);",
"CREATE TABLE attachments (id INTEGER PRIMARY KEY, message_id INTEGER,store_data TEXT," +
"content_uri TEXT, size INTEGER, name TEXT, mime_type TEXT," +
"content_id TEXT, content_disposition TEXT);",
"CREATE TABLE pending_commands (id INTEGER PRIMARY KEY, command TEXT, arguments TEXT);",
"CREATE INDEX folder_name ON folders (name);",
"CREATE INDEX header_folder ON headers (message_id);",
"CREATE INDEX msg_uid ON messages (uid, folder_id);",
"CREATE INDEX msg_folder_id_deleted_date ON messages (folder_id,deleted,internal_date);",
"CREATE INDEX msg_empty ON messages (empty);",
"CREATE INDEX msg_read ON messages (read);",
"CREATE INDEX msg_flagged ON messages (flagged);",
"CREATE INDEX msg_composite ON messages (deleted, empty,folder_id,flagged,read);",
"CREATE INDEX threads_message_id ON threads (message_id);",
"CREATE INDEX threads_root ON threads (root);",
"CREATE INDEX threads_parent ON threads (parent);",
"CREATE TRIGGER set_thread_root AFTER INSERT ON threads BEGIN " +
"UPDATE threads SET root=id WHERE root IS NULL AND ROWID = NEW.ROWID; END;",
"CREATE TRIGGER delete_folder BEFORE DELETE ON folders BEGIN " +
"DELETE FROM messages WHERE old.id = folder_id; END;",
"CREATE TRIGGER delete_message BEFORE DELETE ON messages BEGIN " +
"DELETE FROM attachments WHERE old.id = message_id;" +
"DELETE FROM headers where old.id = message_id; END;"
};
for (String statement : v50SchemaSql) {
db.execSQL(statement);
}
db.setVersion(50);
String[] folderSql = new String[] {
"INSERT INTO folders VALUES (1,'Trash',0,NULL,25,NULL,NULL,0,0,0,1,'NO_CLASS','INHERITED','FIRST_CLASS','INHERITED')",
"INSERT INTO folders VALUES (2,'Sent',1448975758597,NULL,25,NULL,'uidNext=552',NULL,0,0,1,'NO_CLASS','INHERITED','FIRST_CLASS','INHERITED')",
"INSERT INTO folders VALUES (8,'Drafts',0,NULL,25,NULL,NULL,0,0,0,1,'FIRST_CLASS','INHERITED','FIRST_CLASS','INHERITED')",
"INSERT INTO folders VALUES (13,'Spam',NULL,NULL,25,NULL,NULL,NULL,0,0,1,'NO_CLASS','INHERITED','FIRST_CLASS','INHERITED')",
"INSERT INTO folders VALUES (14,'K9MAIL_INTERNAL_OUTBOX',NULL,NULL,25,NULL,NULL,NULL,0,0,1,'NO_CLASS','INHERITED','FIRST_CLASS','INHERITED')",
"INSERT INTO folders VALUES (15,'K9mail-errors',NULL,NULL,25,NULL,NULL,NULL,0,0,1,'NO_CLASS','INHERITED','FIRST_CLASS','INHERITED')",
"INSERT INTO folders VALUES (16,'dev',1453812012958,NULL,25,NULL,'uidNext=9',0,0,0,0,'INHERITED','SECOND_CLASS','NO_CLASS','INHERITED')",
};
for (String statement : folderSql) {
db.execSQL(statement);
}
return db;
}
private void insertSimplePlaintextMessage(SQLiteDatabase db) {
String[] statements = new String[] {
"INSERT INTO messages VALUES(2,0,16,'3','regular mail',1453380493000," +
"'X_GOT_ALL_HEADERS,X_DOWNLOADED_FULL','look@my.amazin.horse;','valodim@mugenguild.com'," +
"'','','','<pre class=\"k9mail\">nothing special here.<br /></pre>','nothing special here.\n'," +
"0,1453380499000,'<20160121124813.GA31046@littlepip>','nothing special here.'," +
"'text/plain',NULL,0,1,0,0,0)",
"INSERT INTO headers (message_id, name, value) VALUES (2,'Return-Path','<look@my.amazin.horse>')",
"INSERT INTO headers (message_id, name, value) VALUES (2,'X-Original-To','valodim@mugenguild.com')",
"INSERT INTO headers (message_id, name, value) VALUES (2,'Delivered-To','valodim@mugenguild.com')",
"INSERT INTO headers (message_id, name, value) VALUES (2,'Date','Thu, 21 Jan 2016 13:48:13 +0100')",
"INSERT INTO headers (message_id, name, value) VALUES (2,'From','Vincent Breitmoser <look@my.amazin.horse>')",
"INSERT INTO headers (message_id, name, value) VALUES (2,'To','valodim@mugenguild.com')",
"INSERT INTO headers (message_id, name, value) VALUES (2,'Subject','regular mail')",
"INSERT INTO headers (message_id, name, value) VALUES (2,'Message-ID','<20160121124813.GA31046@littlepip>')",
"INSERT INTO headers (message_id, name, value) VALUES (2,'Content-Disposition','inline')",
"INSERT INTO headers (message_id, name, value) VALUES (2,'User-Agent','Mutt/1.5.24 (2015-08-30)')",
"INSERT INTO headers (message_id, name, value) VALUES (2,'MIME-Version','1.0')",
"INSERT INTO headers (message_id, name, value) VALUES (2,'Content-Type','text/plain\n charset=utf-8')",
"INSERT INTO headers (message_id, name, value) VALUES (2,'Content-Transfer-Encoding','8bit')",
"INSERT INTO threads VALUES(3,2,3,NULL)",
};
for (String statement : statements) {
db.execSQL(statement);
}
}
@Test
public void migrateTextPlain() throws Exception {
SQLiteDatabase db = createV50Database();
insertSimplePlaintextMessage(db);
db.close();
LocalStore localStore = LocalStore.getInstance(account, RuntimeEnvironment.application);
LocalMessage msg = localStore.getFolder("dev").getMessage("3");
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.BODY);
localStore.getFolder("dev").fetch(Collections.singletonList(msg), fp, null);
Assert.assertEquals("text/plain", msg.getMimeType());
Assert.assertEquals(2, msg.getId());
Assert.assertEquals(13, msg.getHeaderNames().size());
Assert.assertEquals(0, msg.getAttachmentCount());
Assert.assertEquals(1, msg.getHeader("User-Agent").length);
Assert.assertEquals("Mutt/1.5.24 (2015-08-30)", msg.getHeader("User-Agent")[0]);
Assert.assertEquals(1, msg.getHeader(MimeHeader.HEADER_CONTENT_TYPE).length);
Assert.assertEquals("text/plain",
MimeUtility.getHeaderParameter(msg.getHeader(MimeHeader.HEADER_CONTENT_TYPE)[0], null));
Assert.assertEquals("utf-8",
MimeUtility.getHeaderParameter(msg.getHeader(MimeHeader.HEADER_CONTENT_TYPE)[0], "charset"));
Assert.assertTrue(msg.getBody() instanceof BinaryMemoryBody);
String msgTextContent = MessageExtractor.getTextFromPart(msg);
Assert.assertEquals("nothing special here.\r\n", msgTextContent);
}
private void insertMixedWithAttachments(SQLiteDatabase db) throws Exception {
String[] statements = new String[] {
"INSERT INTO messages VALUES(3,0,16,'4','mail with attach',1453380649000," +
"'X_GOT_ALL_HEADERS,X_DOWNLOADED_PARTIAL','look@my.amazin.horse;','valodim@mugenguild.com'," +
"'','','','<pre class=\"k9mail\">ooohh, an attachment!<br /></pre>','ooohh, an attachment!\n'," +
"2,1453380654000,'<20160121125049.GB31046@littlepip>','ooohh, an attachment!'," +
"'multipart/mixed',NULL,0,1,0,0,0)",
"INSERT INTO headers (message_id, name, value) VALUES (3,'Date','Thu, 21 Jan 2016 13:50:49 +0100')",
"INSERT INTO headers (message_id, name, value) VALUES (3,'From','Vincent Breitmoser <look@my.amazin.horse>')",
"INSERT INTO headers (message_id, name, value) VALUES (3,'To','valodim@mugenguild.com')",
"INSERT INTO headers (message_id, name, value) VALUES (3,'Subject','mail with attach')",
"INSERT INTO headers (message_id, name, value) VALUES (3,'Message-ID','<20160121125049.GB31046@littlepip>')",
"INSERT INTO headers (message_id, name, value) VALUES (3,'MIME-Version','1.0')",
"INSERT INTO headers (message_id, name, value) VALUES (3,'Content-Type','multipart/mixed; boundary=\"----5D6OUTIYLNN2X63O0R2M0V53TOUAQP\"')",
"INSERT INTO headers (message_id, name, value) VALUES (3,'Content-Transfer-Encoding','8bit')",
"INSERT INTO threads VALUES(4,3,4,NULL)",
"INSERT INTO attachments VALUES(3,3,'2'," +
"'content://com.fsck.k9.attachmentprovider/" + account.getUuid() + "/3/RAW',2250," +
"'k9small.png','image/png',NULL,'attachment')",
"INSERT INTO attachments VALUES(4,3,'2'," +
"'content://com.fsck.k9.attachmentprovider/" + account.getUuid() + "/5/RAW',2250," +
"'baduri.png','application/whatevs',NULL,'attachment')",
};
for (String statement : statements) {
db.execSQL(statement);
}
copyAttachmentFromFile("k9small.png", 3, 2250);
copyAttachmentFromFile("k9small.png", 5, 2250);
}
@Test
public void migrateMixedWithAttachments() throws Exception {
SQLiteDatabase db = createV50Database();
insertMixedWithAttachments(db);
db.close();
LocalStore localStore = LocalStore.getInstance(account, RuntimeEnvironment.application);
LocalMessage msg = localStore.getFolder("dev").getMessage("4");
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.BODY);
localStore.getFolder("dev").fetch(Collections.singletonList(msg), fp, null);
Assert.assertEquals(3, msg.getId());
Assert.assertEquals(8, msg.getHeaderNames().size());
Assert.assertEquals("multipart/mixed", msg.getMimeType());
Assert.assertEquals(1, msg.getHeader(MimeHeader.HEADER_CONTENT_TYPE).length);
Assert.assertEquals("multipart/mixed",
MimeUtility.getHeaderParameter(msg.getHeader(MimeHeader.HEADER_CONTENT_TYPE)[0], null));
Assert.assertEquals("----5D6OUTIYLNN2X63O0R2M0V53TOUAQP",
MimeUtility.getHeaderParameter(msg.getHeader(MimeHeader.HEADER_CONTENT_TYPE)[0], "boundary"));
Assert.assertEquals(2, msg.getAttachmentCount());
Multipart body = (Multipart) msg.getBody();
Assert.assertEquals(3, body.getCount());
Assert.assertEquals("multipart/alternative", body.getBodyPart(0).getMimeType());
LocalBodyPart attachmentPart = (LocalBodyPart) body.getBodyPart(1);
Assert.assertEquals("image/png", attachmentPart.getMimeType());
Assert.assertEquals("2", attachmentPart.getServerExtra());
Assert.assertEquals("k9small.png", attachmentPart.getDisplayName());
Assert.assertEquals("attachment", MimeUtility.getHeaderParameter(attachmentPart.getDisposition(), null));
Assert.assertEquals("k9small.png", MimeUtility.getHeaderParameter(attachmentPart.getDisposition(), "filename"));
Assert.assertEquals("2250", MimeUtility.getHeaderParameter(attachmentPart.getDisposition(), "size"));
Assert.assertTrue(attachmentPart.isFirstClassAttachment());
FileBackedBody attachmentBody = (FileBackedBody) attachmentPart.getBody();
Assert.assertEquals(2250, attachmentBody.getSize());
Assert.assertEquals(MimeUtil.ENC_BINARY, attachmentBody.getEncoding());
Assert.assertEquals("application/whatevs", body.getBodyPart(2).getMimeType());
Assert.assertNull(body.getBodyPart(2).getBody());
}
private void insertPgpMimeSignedMessage(SQLiteDatabase db) {
String[] statements = new String[] {
"INSERT INTO messages VALUES(4,0,16,'5','signed mail with attach',1453380687000," +
"'X_GOT_ALL_HEADERS,X_DOWNLOADED_PARTIAL','look@my.amazin.horse;','valodim@mugenguild.com'," +
"'','','','<pre class=\"k9mail\">attached AND signed!<br /><br /> - V<br /></pre>','attached AND signed!\n" +
"\n" +
" - V\n" +
"',2,1453380691000,'<20160121125127.GC31046@littlepip>','attached AND signed! - V'," +
"'multipart/signed',NULL,0,1,0,0,0)",
"INSERT INTO headers (message_id, name, value) VALUES (4,'Date','Thu, 21 Jan 2016 13:51:27 +0100')",
"INSERT INTO headers (message_id, name, value) VALUES (4,'From','Vincent Breitmoser <look@my.amazin.horse>')",
"INSERT INTO headers (message_id, name, value) VALUES (4,'To','valodim@mugenguild.com')",
"INSERT INTO headers (message_id, name, value) VALUES (4,'Subject','signed mail with attach')",
"INSERT INTO headers (message_id, name, value) VALUES (4,'Message-ID','<20160121125127.GC31046@littlepip>')",
"INSERT INTO headers (message_id, name, value) VALUES (4,'MIME-Version','1.0')",
"INSERT INTO headers (message_id, name, value) VALUES (4,'Content-Type','multipart/signed; boundary=\"----03N4L9HQP6BY776BVZW4ZIPOWZLBJH\"')",
"INSERT INTO headers (message_id, name, value) VALUES (4,'Content-Transfer-Encoding','8bit')",
"INSERT INTO threads VALUES(5,4,5,NULL)",
"INSERT INTO attachments VALUES(6,4,'2',NULL,836,'signature.asc','application/pgp-signature',NULL,'')",
"INSERT INTO attachments VALUES(5,4,'1.2',NULL,39456,'smirk.png','image/png',NULL,'attachment')",
};
for (String statement : statements) {
db.execSQL(statement);
}
}
@Test
public void migratePgpMimeSignedMessage() throws Exception {
SQLiteDatabase db = createV50Database();
insertPgpMimeSignedMessage(db);
db.close();
LocalStore localStore = LocalStore.getInstance(account, RuntimeEnvironment.application);
LocalMessage msg = localStore.getFolder("dev").getMessage("5");
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.BODY);
localStore.getFolder("dev").fetch(Collections.singletonList(msg), fp, null);
Assert.assertEquals(4, msg.getId());
Assert.assertEquals(8, msg.getHeaderNames().size());
Assert.assertEquals("multipart/mixed", msg.getMimeType());
Assert.assertEquals(2, msg.getAttachmentCount());
Multipart body = (Multipart) msg.getBody();
Assert.assertEquals(3, body.getCount());
Assert.assertEquals("multipart/alternative", body.getBodyPart(0).getMimeType());
Assert.assertEquals("image/png", body.getBodyPart(1).getMimeType());
Assert.assertEquals("application/pgp-signature", body.getBodyPart(2).getMimeType());
}
private void insertPgpMimeEncryptedMessage(SQLiteDatabase db) {
String[] statements = new String[] {
"INSERT INTO messages VALUES(5,0,16,'6','pgp/mime encrypted text',1453380734000," +
"'X_GOT_ALL_HEADERS,X_DOWNLOADED_FULL','look@my.amazin.horse;','valodim@mugenguild.com'," +
"'','','',NULL,NULL,2,1453380737000,'<20160121125214.GD31046@littlepip>',NULL," +
"'multipart/encrypted',NULL,0,1,0,0,0)",
"INSERT INTO headers (message_id, name, value) VALUES (5,'Return-Path','<look@my.amazin.horse>')",
"INSERT INTO headers (message_id, name, value) VALUES (5,'X-Original-To','valodim@mugenguild.com')",
"INSERT INTO headers (message_id, name, value) VALUES (5,'Delivered-To','valodim@mugenguild.com')",
"INSERT INTO headers (message_id, name, value) VALUES (5,'Date','Thu, 21 Jan 2016 13:52:14 +0100')",
"INSERT INTO headers (message_id, name, value) VALUES (5,'From','Vincent Breitmoser <look@my.amazin.horse>')",
"INSERT INTO headers (message_id, name, value) VALUES (5,'To','valodim@mugenguild.com')",
"INSERT INTO headers (message_id, name, value) VALUES (5,'Subject','pgp/mime encrypted text')",
"INSERT INTO headers (message_id, name, value) VALUES (5,'Message-ID','<20160121125214.GD31046@littlepip>')",
"INSERT INTO headers (message_id, name, value) VALUES (5,'Content-Disposition','inline')",
"INSERT INTO headers (message_id, name, value) VALUES (5,'User-Agent','Mutt/1.5.24 (2015-08-30)')",
"INSERT INTO headers (message_id, name, value) VALUES (5,'MIME-Version','1.0')",
"INSERT INTO headers (message_id, name, value) VALUES (5,'Content-Type','multipart/encrypted; protocol=\"application/pgp-encrypted\";\tboundary=\"UoPmpPX/dBe4BELn\"')",
"INSERT INTO headers (message_id, name, value) VALUES (5,'Content-Transfer-Encoding','8bit')",
"INSERT INTO threads VALUES(6,5,6,NULL)",
"INSERT INTO attachments VALUES(1,5,NULL,'content://com.fsck.k9.attachmentprovider/" + account.getUuid() + "/1/RAW',12,NULL,'application/pgp-encrypted',NULL,'attachment')",
"INSERT INTO attachments VALUES(2,5,NULL,'content://com.fsck.k9.attachmentprovider/" + account.getUuid() + "/2/RAW',1946,'msg.asc','application/octet-stream',NULL,'attachment')",
};
for (String statement : statements) {
db.execSQL(statement);
}
}
@Test
public void migratePgpMimeEncryptedMessage() throws Exception {
SQLiteDatabase db = createV50Database();
insertPgpMimeEncryptedMessage(db);
db.close();
LocalStore localStore = LocalStore.getInstance(account, RuntimeEnvironment.application);
LocalMessage msg = localStore.getFolder("dev").getMessage("6");
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.BODY);
localStore.getFolder("dev").fetch(Collections.singletonList(msg), fp, null);
Assert.assertEquals(5, msg.getId());
Assert.assertEquals(13, msg.getHeaderNames().size());
Assert.assertEquals("multipart/encrypted", msg.getMimeType());
Assert.assertEquals(2, msg.getAttachmentCount());
Multipart body = (Multipart) msg.getBody();
Assert.assertEquals(1, msg.getHeader(MimeHeader.HEADER_CONTENT_TYPE).length);
Assert.assertEquals("application/pgp-encrypted",
MimeUtility.getHeaderParameter(msg.getHeader(MimeHeader.HEADER_CONTENT_TYPE)[0], "protocol"));
Assert.assertEquals("UoPmpPX/dBe4BELn",
MimeUtility.getHeaderParameter(msg.getHeader(MimeHeader.HEADER_CONTENT_TYPE)[0], "boundary"));
Assert.assertEquals("UoPmpPX/dBe4BELn", body.getBoundary());
Assert.assertEquals(2, body.getCount());
Assert.assertEquals("application/pgp-encrypted", body.getBodyPart(0).getMimeType());
Assert.assertEquals("application/octet-stream", body.getBodyPart(1).getMimeType());
}
private void insertPgpInlineEncryptedMessage(SQLiteDatabase db) {
String[] statements = new String[] {
"INSERT INTO messages VALUES(6,0,16,'7','pgp/inline encrypted',1453380759000," +
"'X_GOT_ALL_HEADERS,X_DOWNLOADED_FULL','look@my.amazin.horse;','valodim@mugenguild.com'," +
"'','','','<pre class=\"k9mail\">-----BEGIN PGP MESSAGE-----<br />Version: GnuPG v1<br /><br />" +
"hQIMA65sUXMb7rTOAQ//TemIsM3AK2uYT8P5R4vJSqRkdyr8T0sg0R/xtr7oHY19<br />" +
"fv1t9yu9Z0zub5v4+AhcJ7ZbURUG+ETGsrBS7xJhHxlCu0KQYEme6tnBOrkXN0Tn<br />" +
"9h52EiK7ENbZ53IuBael3XoEWrpC/1nZGSpjvUt+DUC7+OdVGHWMfoxjNMKNcJiT<br />" +
"quVDpaiI2yqDCvHOn9yLlxlZa+j82sSwOb285txTOQWhCY6H1bllAByOiGpQGp6F<br />" +
"FMWsts7y04VoFTwxadzWywi1Bdscd8HFDm0TO/75OKcUVoGRcNuHszxGxSgT+R77<br />" +
"eB0wgXLZQ1NnqWTOGekGJ0x9Ddx8FcMIcokDGUh2D98jJZwVa3I9ssKFUORMCCaN<br />" +
"sAe2xHk3q+tr7mm5qPD7DU3Ld7qotjBVoXlo/jNLbK2drP9wARZV0u6395zp38QR<br />" +
"zp4yattwqpNri4GRG/hD2bM2rD+pQKtCnwqW5VlW2oh2NQ5ztf9eZ5oJgw8clljH<br />" +
"ciJi5B0tuVygJnI/hHy+N4TE8mDAN6IXSBnR570zX6Idb8tiAVSlkdh89wZJ1m7G<br />" +
"nvU7HlNyW5cf6C5RWz+gGsg1aeNQqZGCJkfdDXXd4rpzNsp5LDmzPMwj3f8QIHhi<br />" +
"vsCBaMvS1wiYGjTlAXSbcLVDZ1kyTNd92H1ktC1/A5bUDLZe7EhzHBMDh6YI9k3S<br />" +
"6QFx2E8otLIICwu8lEVbVqdvFkApfpUt8DEBbXxxiw1c8Mbe6EmgTD4H8I6NxwXd<br />" +
"7QGerWOrPzyKjvdBQkmKvfao2fdvPWqsu+tgejmMlQQ4t75zV6Tb75gQOacDUr0w<br />" +
"66D0t/EMp/KL2roBpw3purEubaYsQpQImBiyxpJbLKNL0dxT16V7xa4XUlon8EI+<br />" +
"N/gTZdCzmVUmX5mPRheZaNn0y7/TiMTxMxO+oKDGt0ks4Hqvz9+lphcfqTl2Nhop<br />" +
"AW79xOh8hD2+XIxiNiyRYgugCDm/iSixKStjyV822/6DBltWZ9r1OgeNFBgpIZiO<br />" +
"r5SA4oM0krMljpE+9wCHZt3R4PxJ6Pv+9cxb1MC5tWRO7SKIrp53TZtiqDVTARGh<br />" +
"FgoJGrdL1jo89efPiZJY8mijtPuYk0gwSbTnE7mOaIoczzQr99ojwso1T2CeXIP0<br />" +
"Eg3sCv/0b+fTQlFCRQxvuUaQ75NfhUnA7dBFYCdBtrje/eREO4I/Jg05pb+pp81n<br />" +
"T/QGVl6uA5+zm3YdRSvZ5BIpZleu/ddkvH1a7/113XUmPun397NBC1X0RTa2h6X6<br />" +
"HGPTgqaQ89FJfU3oYvfvEmo8hrKPmaPR+3AXgSCkAGWM+xRddzFAxf72S+LrFaZX<br />" +
"mRf25pDoZf8i2PgsMd2cFcJdO01J6sdtIsm8k9mfk2uVwAFaUBBAgBHZCFzGp3yt<br />" +
"0OIiPTFKywtLMIfqla6hDEoPb+yosiRI9lQmGyW8bOCwO5sMUvFZfTAJnhQvRazS<br />" +
"HWeTlYCKZadM4p2p/ucFAm94edi+DPz2bzaFg7O/+B9N2g/s7PvD0djJEHGGDT+S<br />" +
"ucdGTWliAnOaFCyGUWXmAE7C1O4m+4bJwVmz7ts0ReLwDCGhPmA2/+F/K9WgaU1f<br />" +
"j8JjG3kNUmcrXP0PEctwdi9phnJscL5abfOrI9mT3eYfXIVy<br />" +
"=tD4Z<br />" +
"-----END PGP MESSAGE-----<br />" +
"</pre>','-----BEGIN PGP MESSAGE-----\n" +
"Version: GnuPG v1\n" +
"\n" +
"hQIMA65sUXMb7rTOAQ//TemIsM3AK2uYT8P5R4vJSqRkdyr8T0sg0R/xtr7oHY19\n" +
"fv1t9yu9Z0zub5v4+AhcJ7ZbURUG+ETGsrBS7xJhHxlCu0KQYEme6tnBOrkXN0Tn\n" +
"9h52EiK7ENbZ53IuBael3XoEWrpC/1nZGSpjvUt+DUC7+OdVGHWMfoxjNMKNcJiT\n" +
"quVDpaiI2yqDCvHOn9yLlxlZa+j82sSwOb285txTOQWhCY6H1bllAByOiGpQGp6F\n" +
"FMWsts7y04VoFTwxadzWywi1Bdscd8HFDm0TO/75OKcUVoGRcNuHszxGxSgT+R77\n" +
"eB0wgXLZQ1NnqWTOGekGJ0x9Ddx8FcMIcokDGUh2D98jJZwVa3I9ssKFUORMCCaN\n" +
"sAe2xHk3q+tr7mm5qPD7DU3Ld7qotjBVoXlo/jNLbK2drP9wARZV0u6395zp38QR\n" +
"zp4yattwqpNri4GRG/hD2bM2rD+pQKtCnwqW5VlW2oh2NQ5ztf9eZ5oJgw8clljH\n" +
"ciJi5B0tuVygJnI/hHy+N4TE8mDAN6IXSBnR570zX6Idb8tiAVSlkdh89wZJ1m7G\n" +
"nvU7HlNyW5cf6C5RWz+gGsg1aeNQqZGCJkfdDXXd4rpzNsp5LDmzPMwj3f8QIHhi\n" +
"vsCBaMvS1wiYGjTlAXSbcLVDZ1kyTNd92H1ktC1/A5bUDLZe7EhzHBMDh6YI9k3S\n" +
"6QFx2E8otLIICwu8lEVbVqdvFkApfpUt8DEBbXxxiw1c8Mbe6EmgTD4H8I6NxwXd\n" +
"7QGerWOrPzyKjvdBQkmKvfao2fdvPWqsu+tgejmMlQQ4t75zV6Tb75gQOacDUr0w\n" +
"66D0t/EMp/KL2roBpw3purEubaYsQpQImBiyxpJbLKNL0dxT16V7xa4XUlon8EI+\n" +
"N/gTZdCzmVUmX5mPRheZaNn0y7/TiMTxMxO+oKDGt0ks4Hqvz9+lphcfqTl2Nhop\n" +
"AW79xOh8hD2+XIxiNiyRYgugCDm/iSixKStjyV822/6DBltWZ9r1OgeNFBgpIZiO\n" +
"r5SA4oM0krMljpE+9wCHZt3R4PxJ6Pv+9cxb1MC5tWRO7SKIrp53TZtiqDVTARGh\n" +
"FgoJGrdL1jo89efPiZJY8mijtPuYk0gwSbTnE7mOaIoczzQr99ojwso1T2CeXIP0\n" +
"Eg3sCv/0b+fTQlFCRQxvuUaQ75NfhUnA7dBFYCdBtrje/eREO4I/Jg05pb+pp81n\n" +
"T/QGVl6uA5+zm3YdRSvZ5BIpZleu/ddkvH1a7/113XUmPun397NBC1X0RTa2h6X6\n" +
"HGPTgqaQ89FJfU3oYvfvEmo8hrKPmaPR+3AXgSCkAGWM+xRddzFAxf72S+LrFaZX\n" +
"mRf25pDoZf8i2PgsMd2cFcJdO01J6sdtIsm8k9mfk2uVwAFaUBBAgBHZCFzGp3yt\n" +
"0OIiPTFKywtLMIfqla6hDEoPb+yosiRI9lQmGyW8bOCwO5sMUvFZfTAJnhQvRazS\n" +
"HWeTlYCKZadM4p2p/ucFAm94edi+DPz2bzaFg7O/+B9N2g/s7PvD0djJEHGGDT+S\n" +
"ucdGTWliAnOaFCyGUWXmAE7C1O4m+4bJwVmz7ts0ReLwDCGhPmA2/+F/K9WgaU1f\n" +
"j8JjG3kNUmcrXP0PEctwdi9phnJscL5abfOrI9mT3eYfXIVy\n" +
"=tD4Z\n" +
"-----END PGP MESSAGE-----\n" +
"',0,1453380763000,'<20160121125239.GE31046@littlepip>','Version: GnuPG v1 hQIMA65sUXMb7rTOAQ//TemIsM3AK2uYT8P5R4vJSqRkdyr8T0sg0R/xtr7oHY19 fv1t9yu9Z0zub5v4+AhcJ7ZbURUG+ETGsrBS7xJhHxlCu0KQYEme6tnBOrkXN0Tn 9h52EiK7ENbZ53IuBael3XoEWrpC/1nZGSpjvUt+DUC7+OdVGHWMfoxjNMKNcJiT quVDpaiI2yqDCvHOn9yLlxlZa+j82sSwOb285txTOQWhCY6H1bllAByOiGpQGp6F FMWsts7y04VoFTwxadzWywi1Bdscd8HFDm0TO/75OKcUVoGRcNuHszxGxSgT+R77 eB0wgXLZQ1NnqWTOGekGJ0x9Ddx8FcMIcokDGUh2D98jJZwVa3I9ssKFUORMCCaN sAe2xHk3q+tr7mm5qPD7DU3Ld7qotjBVoXlo/jNLbK2drP9wARZV0u6395zp38QR zp4yattwqpNri4GRG/hD2bM2rD+pQKtCnwqW5Vl','text/plain',NULL,0,1,0,0,0)",
"INSERT INTO headers (message_id, name, value) VALUES (6,'Return-Path','<look@my.amazin.horse>')",
"INSERT INTO headers (message_id, name, value) VALUES (6,'X-Original-To','valodim@mugenguild.com')",
"INSERT INTO headers (message_id, name, value) VALUES (6,'Delivered-To','valodim@mugenguild.com')",
"INSERT INTO headers (message_id, name, value) VALUES (6,'Date','Thu, 21 Jan 2016 13:52:39 +0100')",
"INSERT INTO headers (message_id, name, value) VALUES (6,'From','Vincent Breitmoser <look@my.amazin.horse>')",
"INSERT INTO headers (message_id, name, value) VALUES (6,'To','valodim@mugenguild.com')",
"INSERT INTO headers (message_id, name, value) VALUES (6,'Subject','pgp/inline encrypted')",
"INSERT INTO headers (message_id, name, value) VALUES (6,'Message-ID','<20160121125239.GE31046@littlepip>')",
"INSERT INTO headers (message_id, name, value) VALUES (6,'User-Agent','Mutt/1.5.24 (2015-08-30)')",
"INSERT INTO headers (message_id, name, value) VALUES (6,'MIME-Version','1.0')",
"INSERT INTO headers (message_id, name, value) VALUES (6,'Content-Type','text/plain\n charset=utf-8')",
"INSERT INTO headers (message_id, name, value) VALUES (6,'Content-Transfer-Encoding','8bit')",
"INSERT INTO threads VALUES(7,6,7,NULL)",
};
for (String statement : statements) {
db.execSQL(statement);
}
}
@Test
public void migratePgpInlineEncryptedMessage() throws Exception {
SQLiteDatabase db = createV50Database();
insertPgpInlineEncryptedMessage(db);
db.close();
LocalStore localStore = LocalStore.getInstance(account, RuntimeEnvironment.application);
LocalMessage msg = localStore.getFolder("dev").getMessage("7");
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.BODY);
localStore.getFolder("dev").fetch(Collections.singletonList(msg), fp, null);
Assert.assertEquals(6, msg.getId());
Assert.assertEquals(12, msg.getHeaderNames().size());
Assert.assertEquals("text/plain", msg.getMimeType());
Assert.assertEquals(0, msg.getAttachmentCount());
Assert.assertTrue(msg.getBody() instanceof BinaryMemoryBody);
String msgTextContent = MessageExtractor.getTextFromPart(msg);
Assert.assertEquals(OpenPgpUtils.PARSE_RESULT_MESSAGE, OpenPgpUtils.parseMessage(msgTextContent));
}
private void insertPgpInlineClearsignedMessage(SQLiteDatabase db) {
String[] statements = new String[] {
"INSERT INTO messages VALUES(7,0,16,'8','pgp/inline clearsigned',1453380782000," +
"'X_GOT_ALL_HEADERS,X_DOWNLOADED_FULL','look@my.amazin.horse;','valodim@mugenguild.com'," +
"'','','','<pre class=\"k9mail\">-----BEGIN PGP SIGNED MESSAGE-----<br />Hash: SHA1<br /><br />" +
"this msg is only signed~<br /><br />-----BEGIN PGP SIGNATURE-----<br />Version: GnuPG v1<br />" +
"<br />iQIcBAEBAgAGBQJWoNSuAAoJEHvRgyDerfoROjkQAK2Md7CE4GDcHaWppXUttUeh<br />" +
"wrjbnW2McJSysjWmb6FYt1CYsjl+3vImIgqg59rZxjdaffs+lNxO3B0blfAOMjSs<br />" +
"LOCJytbB02/f79e44kWWt5ZG0d+3NTl8sN4OkXb47fot28CG7JLJgkGpMbmwm6sM<br />" +
"C5pUA4o/OwWbkg2xj2FUDmgx4clyA9BxEBxO1ZU+VFLawtZ6OdRLF8iKzJKyKTi4<br />" +
"GEQEiSET5UcRMFgUeI6U3fLPKnmSer4qZP8/G9IcvpVgCOzW6foMZ8mbO+n/Jqs4<br />" +
"644slRlBNYor/5tl5f6sYy5Hyzrj4c6Tq2Duzu0VECQnaTOCl7QyW8Vc1R2qferO<br />" +
"4Rs94InVWfNn5ltV7OPHLBSNAZ8YRILpafrWw+EZbrE5+hwlKernpdn6dRAG668s<br />" +
"KyASsXjtGfPUlcYtFvJQS2U/gAsGcQPPL9g4x8FL2jRqDI92EU8Cw+G2HKlqNegP<br />" +
"6vNkfGv4/LRCtQ+KrajFcSyqrjmZV8lohCI3qJowtK5nFN3Z+5Kk2jqVgRHYuXhR<br />" +
"uCcQrwHOvVts+POHWqbPOR3VDaGWS40rqAwaJrko92IOxhEpUBmNnpH2dARKs1AB<br />" +
"itiWpWNkbalgbEDBx4mmdcj4KsVF5Q86xfg3n8zUQAqhoOKwll5wRQ11lQOXca6O<br />" +
"GsPI+A/j12owLSxOez//<br />=Umj+<br />-----END PGP SIGNATURE-----<br />" +
"</pre>','-----BEGIN PGP SIGNED MESSAGE-----\n" +
"Hash: SHA1\n" +
"\n" +
"this msg is only signed~\n" +
"\n" +
"-----BEGIN PGP SIGNATURE-----\n" +
"Version: GnuPG v1\n" +
"\n" +
"iQIcBAEBAgAGBQJWoNSuAAoJEHvRgyDerfoROjkQAK2Md7CE4GDcHaWppXUttUeh\n" +
"wrjbnW2McJSysjWmb6FYt1CYsjl+3vImIgqg59rZxjdaffs+lNxO3B0blfAOMjSs\n" +
"LOCJytbB02/f79e44kWWt5ZG0d+3NTl8sN4OkXb47fot28CG7JLJgkGpMbmwm6sM\n" +
"C5pUA4o/OwWbkg2xj2FUDmgx4clyA9BxEBxO1ZU+VFLawtZ6OdRLF8iKzJKyKTi4\n" +
"GEQEiSET5UcRMFgUeI6U3fLPKnmSer4qZP8/G9IcvpVgCOzW6foMZ8mbO+n/Jqs4\n" +
"644slRlBNYor/5tl5f6sYy5Hyzrj4c6Tq2Duzu0VECQnaTOCl7QyW8Vc1R2qferO\n" +
"4Rs94InVWfNn5ltV7OPHLBSNAZ8YRILpafrWw+EZbrE5+hwlKernpdn6dRAG668s\n" +
"KyASsXjtGfPUlcYtFvJQS2U/gAsGcQPPL9g4x8FL2jRqDI92EU8Cw+G2HKlqNegP\n" +
"6vNkfGv4/LRCtQ+KrajFcSyqrjmZV8lohCI3qJowtK5nFN3Z+5Kk2jqVgRHYuXhR\n" +
"uCcQrwHOvVts+POHWqbPOR3VDaGWS40rqAwaJrko92IOxhEpUBmNnpH2dARKs1AB\n" +
"itiWpWNkbalgbEDBx4mmdcj4KsVF5Q86xfg3n8zUQAqhoOKwll5wRQ11lQOXca6O\n" +
"GsPI+A/j12owLSxOez//\n" +
"=Umj+\n" +
"-----END PGP SIGNATURE-----\n" +
"',0,1453380785000,'<20160121125302.GF31046@littlepip>','Hash: SHA1 this msg is only signed~ Version: GnuPG v1 iQIcBAEBAgAGBQJWoNSuAAoJEHvRgyDerfoROjkQAK2Md7CE4GDcHaWppXUttUeh wrjbnW2McJSysjWmb6FYt1CYsjl+3vImIgqg59rZxjdaffs+lNxO3B0blfAOMjSs LOCJytbB02/f79e44kWWt5ZG0d+3NTl8sN4OkXb47fot28CG7JLJgkGpMbmwm6sM C5pUA4o/OwWbkg2xj2FUDmgx4clyA9BxEBxO1ZU+VFLawtZ6OdRLF8iKzJKyKTi4 GEQEiSET5UcRMFgUeI6U3fLPKnmSer4qZP8/G9IcvpVgCOzW6foMZ8mbO+n/Jqs4 644slRlBNYor/5tl5f6sYy5Hyzrj4c6Tq2Duzu0VECQnaTOCl7QyW8Vc1R2qferO 4Rs94InVWfNn5ltV7OPHLBSNAZ8YRILpafrWw+EZbrE5+hwlKernpdn6dRAG668s KyA','text/plain',NULL,0,1,0,0,0)",
"INSERT INTO headers (message_id, name, value) VALUES (7,'Return-Path','<look@my.amazin.horse>')",
"INSERT INTO headers (message_id, name, value) VALUES (7,'X-Original-To','valodim@mugenguild.com')",
"INSERT INTO headers (message_id, name, value) VALUES (7,'Delivered-To','valodim@mugenguild.com')",
"INSERT INTO headers (message_id, name, value) VALUES (7,'Date','Thu, 21 Jan 2016 13:53:02 +0100')",
"INSERT INTO headers (message_id, name, value) VALUES (7,'From','Vincent Breitmoser <look@my.amazin.horse>')",
"INSERT INTO headers (message_id, name, value) VALUES (7,'To','valodim@mugenguild.com')",
"INSERT INTO headers (message_id, name, value) VALUES (7,'Subject','pgp/inline clearsigned')",
"INSERT INTO headers (message_id, name, value) VALUES (7,'Message-ID','<20160121125302.GF31046@littlepip>')",
"INSERT INTO headers (message_id, name, value) VALUES (7,'User-Agent','Mutt/1.5.24 (2015-08-30)')",
"INSERT INTO headers (message_id, name, value) VALUES (7,'MIME-Version','1.0')",
"INSERT INTO headers (message_id, name, value) VALUES (7,'Content-Type','text/plain\n charset=utf-8')",
"INSERT INTO headers (message_id, name, value) VALUES (7,'Content-Transfer-Encoding','8bit')",
"INSERT INTO threads VALUES(8,7,8,NULL)",
};
for (String statement : statements) {
db.execSQL(statement);
}
}
@Test
public void migratePgpInlineClearsignedMessage() throws Exception {
SQLiteDatabase db = createV50Database();
insertPgpInlineClearsignedMessage(db);
db.close();
LocalStore localStore = LocalStore.getInstance(account, RuntimeEnvironment.application);
LocalMessage msg = localStore.getFolder("dev").getMessage("8");
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.BODY);
localStore.getFolder("dev").fetch(Collections.singletonList(msg), fp, null);
Assert.assertEquals(7, msg.getId());
Assert.assertEquals(12, msg.getHeaderNames().size());
Assert.assertEquals("text/plain", msg.getMimeType());
Assert.assertEquals(0, msg.getAttachmentCount());
Assert.assertTrue(msg.getBody() instanceof BinaryMemoryBody);
String msgTextContent = MessageExtractor.getTextFromPart(msg);
Assert.assertEquals(OpenPgpUtils.PARSE_RESULT_SIGNED_MESSAGE, OpenPgpUtils.parseMessage(msgTextContent));
}
private void insertMultipartAlternativeMessage(SQLiteDatabase db) {
String[] statements = new String[] {
"INSERT INTO messages VALUES(8,0,16,'9','mail with html multipart',1453922449000," +
"'X_GOT_ALL_HEADERS,X_DOWNLOADED_FULL','jh@example.org;','look@my.amazin.horse'," +
"'','','','<html>\n" +
" <head>\n" +
"\n" +
" <meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\">\n" +
" </head>\n" +
" <body text=\"#000000\" bgcolor=\"#FFFFFF\">\n" +
" <i>this</i> is an <b>HTML-<u>E-MAIL</u></b>.<br>\n" +
" </body>\n" +
"</html>\n" +
"','this is an *HTML-_E-MAIL_*.\n" +
"',0,1453922455000,'<56A91891.7010509@example.org>'," +
"'this is an HTML-E-Mail.','multipart/alternative',NULL,0,0,0,0,0)",
"INSERT INTO headers (message_id, name, value) VALUES (8,'To','look@my.amazin.horse')",
"INSERT INTO headers (message_id, name, value) VALUES (8,'From','=?UTF-8?Q?Jan_H=c3=benbecker?= <jh@example.org>')",
"INSERT INTO headers (message_id, name, value) VALUES (8,'Subject','mail with html multipart')",
"INSERT INTO headers (message_id, name, value) VALUES (8,'Message-ID','<56A91891.7010509@example.org>')",
"INSERT INTO headers (message_id, name, value) VALUES (8,'Date','Wed, 27 Jan 2016 20:20:49 +0100')",
"INSERT INTO headers (message_id, name, value) VALUES (8,'User-Agent','Mozilla/5.0 (X11; Linux x86_64; rv:38.0) Gecko/20100101 Icedove/38.5.0')",
"INSERT INTO headers (message_id, name, value) VALUES (8,'MIME-Version','1.0')",
"INSERT INTO headers (message_id, name, value) VALUES (8,'Content-Type','multipart/alternative; boundary=\"------------060200010509000000040004\"')",
"INSERT INTO headers (message_id, name, value) VALUES (8,'Content-Transfer-Encoding','8bit')",
"INSERT INTO threads VALUES(9,8,9,NULL)",
};
for (String statement : statements) {
db.execSQL(statement);
}
}
@Test
public void migrateTextHtml() throws Exception {
SQLiteDatabase db = createV50Database();
insertMultipartAlternativeMessage(db);
db.close();
LocalStore localStore = LocalStore.getInstance(account, RuntimeEnvironment.application);
LocalMessage msg = localStore.getFolder("dev").getMessage("9");
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.BODY);
localStore.getFolder("dev").fetch(Collections.singletonList(msg), fp, null);
Assert.assertEquals(8, msg.getId());
Assert.assertEquals(9, msg.getHeaderNames().size());
Assert.assertEquals("multipart/alternative", msg.getMimeType());
Assert.assertEquals(0, msg.getAttachmentCount());
Multipart msgBody = (Multipart) msg.getBody();
Assert.assertEquals("------------060200010509000000040004", msgBody.getBoundary());
}
private void insertHtmlWithRelatedMessage(SQLiteDatabase db) {
String[] statements = new String[] {
"INSERT INTO messages VALUES(9,0,16,'10','html with multipart/related content',1453922845000," +
"'X_GOT_ALL_HEADERS,X_DOWNLOADED_FULL','jh@example.org;','look@my.amazin.horse'," +
"'','','','<html>\n" +
" <head>\n" +
"\n" +
" <meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\">\n" +
" </head>\n" +
" <body text=\"#000000\" bgcolor=\"#FFFFFF\">\n" +
" <blink>html text with inline attachment</blink><br>\n" +
" <img alt=\"Alternative_Text\" src=\"content://com.fsck.k9.attachmentprovider/" + account.getUuid() + "/6/RAW\"\n" +
" height=\"177\" width=\"220\">\n" +
" </body>\n" +
"</html>\n" +
"','html text with inline attachment\n" +
"\n" +
"Alternative_Text\n" +
"',1,1453922852000,'<56A91A1D.7050908@example.org>','html text with inline attachment Alternative_Text','multipart/alternative',NULL,0,1,0,0,0);\n",
"INSERT INTO headers (message_id, name, value) VALUES(9,'Return-Path','<jh@example.org>')",
"INSERT INTO headers (message_id, name, value) VALUES(9,'X-Original-To','look@my.amazin.horse')",
"INSERT INTO headers (message_id, name, value) VALUES(9,'To','look@my.amazin.horse')",
"INSERT INTO headers (message_id, name, value) VALUES(9,'From','=?UTF-8?Q?Jan_H=c3=benbecker?= <jh@example.org>')",
"INSERT INTO headers (message_id, name, value) VALUES(9,'Subject','html with multipart/related content')",
"INSERT INTO headers (message_id, name, value) VALUES(9,'Message-ID','<56A91A1D.7050908@example.org>')",
"INSERT INTO headers (message_id, name, value) VALUES(9,'Date','Wed, 27 Jan 2016 20:27:25 +0100')",
"INSERT INTO headers (message_id, name, value) VALUES(9,'User-Agent','Mozilla/5.0 (X11; Linux x86_64; rv:38.0) Gecko/20100101 Icedove/38.5.0')",
"INSERT INTO headers (message_id, name, value) VALUES(9,'MIME-Version','1.0')",
"INSERT INTO headers (message_id, name, value) VALUES(9,'Content-Type','multipart/alternative; boundary=\"------------050707070308090509030605\"')",
"INSERT INTO headers (message_id, name, value) VALUES(9,'Content-Transfer-Encoding','8bit')",
"INSERT INTO threads VALUES(10,9,10,NULL)",
"INSERT INTO attachments VALUES(6,9,NULL,'content://com.fsck.k9.attachmentprovider/" + account.getUuid() + "/6/RAW'," +
"8503,'attached.jpg','image/jpeg','part1.07090108.09020601@example.org','inline')",
};
for (String statement : statements) {
db.execSQL(statement);
}
}
@Test
public void migrateHtmlWithRelatedMessage() throws Exception {
SQLiteDatabase db = createV50Database();
insertHtmlWithRelatedMessage(db);
db.close();
LocalStore localStore = LocalStore.getInstance(account, RuntimeEnvironment.application);
LocalMessage msg = localStore.getFolder("dev").getMessage("10");
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.BODY);
localStore.getFolder("dev").fetch(Collections.singletonList(msg), fp, null);
Assert.assertEquals(9, msg.getId());
Assert.assertEquals(11, msg.getHeaderNames().size());
Assert.assertEquals("multipart/mixed", msg.getMimeType());
Assert.assertEquals(1, msg.getAttachmentCount());
Multipart msgBody = (Multipart) msg.getBody();
Assert.assertEquals("------------050707070308090509030605", msgBody.getBoundary());
Multipart multipartAlternativePart = (Multipart) msgBody.getBodyPart(0).getBody();
BodyPart htmlPart = multipartAlternativePart.getBodyPart(1);
String msgTextContent = MessageExtractor.getTextFromPart(htmlPart);
Assert.assertNotNull(msgTextContent);
Assert.assertTrue(msgTextContent.contains("cid:part1.07090108.09020601@example.org"));
Assert.assertEquals("image/jpeg", msgBody.getBodyPart(1).getMimeType());
}
private void copyAttachmentFromFile(String resourceName, int attachmentId, int expectedFilesize) throws IOException {
File resourceFile = new File(getClass().getResource("/attach/" + resourceName).getFile());
File attachmentFile = new File(attachmentDir, Integer.toString(attachmentId));
BufferedInputStream input = new BufferedInputStream(new FileInputStream(resourceFile));
BufferedOutputStream output = new BufferedOutputStream(new FileOutputStream(attachmentFile));
int copied = IOUtils.copy(input, output);
input.close();
output.close();
Assert.assertEquals(expectedFilesize, copied);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB